From abdff14f69f7a204ef0280776c17484b4e52126f Mon Sep 17 00:00:00 2001 From: Scott Moreau Date: Wed, 16 Sep 2020 16:13:40 -0600 Subject: [PATCH] Add basic clipboard support Uses wlr-data-control-unstable-v1 protocol to interface with the clipboard making copy/paste of text possible. --- include/data-control.h | 40 +++ meson.build | 1 + protocols/meson.build | 1 + protocols/wlr-data-control-unstable-v1.xml | 278 +++++++++++++++++++++ src/data-control.c | 253 +++++++++++++++++++ src/main.c | 24 ++ 6 files changed, 597 insertions(+) create mode 100644 include/data-control.h create mode 100644 protocols/wlr-data-control-unstable-v1.xml create mode 100644 src/data-control.c diff --git a/include/data-control.h b/include/data-control.h new file mode 100644 index 0000000..6ff27d9 --- /dev/null +++ b/include/data-control.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 Scott Moreau + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#pragma once + +#include + +#include "wlr-data-control-unstable-v1.h" + +struct data_control { + struct wl_display* wl_display; + struct nvnc* server; + struct zwlr_data_control_manager_v1* manager; + struct zwlr_data_control_device_v1* data_control_device; + struct zwlr_data_control_source_v1* data_control_source; + struct zwlr_data_control_offer_v1* offer; + struct zwlr_data_control_offer_v1* selection; + struct zwlr_data_control_offer_v1* primary_selection; + char* mime_type; + char* cb_data; + size_t cb_len; + bool hack_to_workaround_wlroots_offering_us_the_source_selection_we_sent; +}; + +void data_control_init(struct data_control* self, struct wl_display* wl_display, struct nvnc* server, struct wl_seat* seat); +void data_control_destroy(struct data_control* self); +void data_control_to_clipboard(struct data_control* self, const char* text, size_t len); diff --git a/meson.build b/meson.build index b3e3869..af2bc15 100644 --- a/meson.build +++ b/meson.build @@ -73,6 +73,7 @@ sources = [ 'src/strlcpy.c', 'src/shm.c', 'src/screencopy.c', + 'src/data-control.c', 'src/output.c', 'src/pointer.c', 'src/keyboard.c', diff --git a/protocols/meson.build b/protocols/meson.build index fdcc4e3..0c0d529 100644 --- a/protocols/meson.build +++ b/protocols/meson.build @@ -20,6 +20,7 @@ client_protocols = [ 'virtual-keyboard-unstable-v1.xml', 'xdg-output-unstable-v1.xml', 'linux-dmabuf-unstable-v1.xml', + 'wlr-data-control-unstable-v1.xml', ] client_protos_src = [] diff --git a/protocols/wlr-data-control-unstable-v1.xml b/protocols/wlr-data-control-unstable-v1.xml new file mode 100644 index 0000000..75e8671 --- /dev/null +++ b/protocols/wlr-data-control-unstable-v1.xml @@ -0,0 +1,278 @@ + + + + Copyright © 2018 Simon Ser + Copyright © 2019 Ivan Molodetskikh + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + This protocol allows a privileged client to control data devices. In + particular, the client will be able to manage the current selection and take + the role of a clipboard manager. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This interface is a manager that allows creating per-seat data device + controls. + + + + + Create a new data source. + + + + + + + Create a data device that can be used to manage a seat's selection. + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + This interface allows a client to manage a seat's selection. + + When the seat is destroyed, this object becomes inert. + + + + + This request asks the compositor to set the selection to the data from + the source on behalf of the client. + + The given source may not be used in any further set_selection or + set_primary_selection requests. Attempting to use a previously used + source is a protocol error. + + To unset the selection, set the source to NULL. + + + + + + + Destroys the data device object. + + + + + + The data_offer event introduces a new wlr_data_control_offer object, + which will subsequently be used in either the + wlr_data_control_device.selection event (for the regular clipboard + selections) or the wlr_data_control_device.primary_selection event (for + the primary clipboard selections). Immediately following the + wlr_data_control_device.data_offer event, the new data_offer object + will send out wlr_data_control_offer.offer events to describe the MIME + types it offers. + + + + + + + The selection event is sent out to notify the client of a new + wlr_data_control_offer for the selection for this device. The + wlr_data_control_device.data_offer and the wlr_data_control_offer.offer + events are sent out immediately before this event to introduce the data + offer object. The selection event is sent to a client when a new + selection is set. The wlr_data_control_offer is valid until a new + wlr_data_control_offer or NULL is received. The client must destroy the + previous selection wlr_data_control_offer, if any, upon receiving this + event. + + The first selection event is sent upon binding the + wlr_data_control_device object. + + + + + + + This data control object is no longer valid and should be destroyed by + the client. + + + + + + + + The primary_selection event is sent out to notify the client of a new + wlr_data_control_offer for the primary selection for this device. The + wlr_data_control_device.data_offer and the wlr_data_control_offer.offer + events are sent out immediately before this event to introduce the data + offer object. The primary_selection event is sent to a client when a + new primary selection is set. The wlr_data_control_offer is valid until + a new wlr_data_control_offer or NULL is received. The client must + destroy the previous primary selection wlr_data_control_offer, if any, + upon receiving this event. + + If the compositor supports primary selection, the first + primary_selection event is sent upon binding the + wlr_data_control_device object. + + + + + + + This request asks the compositor to set the primary selection to the + data from the source on behalf of the client. + + The given source may not be used in any further set_selection or + set_primary_selection requests. Attempting to use a previously used + source is a protocol error. + + To unset the primary selection, set the source to NULL. + + The compositor will ignore this request if it does not support primary + selection. + + + + + + + + + + + + The wlr_data_control_source object is the source side of a + wlr_data_control_offer. It is created by the source client in a data + transfer and provides a way to describe the offered data and a way to + respond to requests to transfer the data. + + + + + + + + + This request adds a MIME type to the set of MIME types advertised to + targets. Can be called several times to offer multiple types. + + Calling this after wlr_data_control_device.set_selection is a protocol + error. + + + + + + + Destroys the data source object. + + + + + + Request for data from the client. Send the data as the specified MIME + type over the passed file descriptor, then close it. + + + + + + + + This data source is no longer valid. The data source has been replaced + by another data source. + + The client should clean up and destroy this data source. + + + + + + + A wlr_data_control_offer represents a piece of data offered for transfer + by another client (the source client). The offer describes the different + MIME types that the data can be converted to and provides the mechanism + for transferring the data directly from the source client. + + + + + To transfer the offered data, the client issues this request and + indicates the MIME type it wants to receive. The transfer happens + through the passed file descriptor (typically created with the pipe + system call). The source client writes the data in the MIME type + representation requested and then closes the file descriptor. + + The receiving client reads from the read end of the pipe until EOF and + then closes its end, at which point the transfer is complete. + + This request may happen multiple times for different MIME types. + + + + + + + + Destroys the data offer object. + + + + + + Sent immediately after creating the wlr_data_control_offer object. + One event per offered MIME type. + + + + + diff --git a/src/data-control.c b/src/data-control.c new file mode 100644 index 0000000..2d4028a --- /dev/null +++ b/src/data-control.c @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2020 Scott Moreau + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#include "logging.h" +#include "data-control.h" + +static void receive_data(void* data, + struct zwlr_data_control_offer_v1* zwlr_data_control_offer_v1, + const char* mime_type) +{ + struct data_control* self = data; + char buf[4096]; + FILE* mem_fp; + char* mem_data; + size_t mem_size = 0; + int pipe_fd[2]; + int ret; + + if (pipe(pipe_fd) == -1) { + log_error("pipe() failed: %m\n"); + return; + } + + zwlr_data_control_offer_v1_receive(zwlr_data_control_offer_v1, mime_type, pipe_fd[1]); + wl_display_flush(self->wl_display); + close(pipe_fd[1]); + + mem_fp = open_memstream(&mem_data, &mem_size); + if (!mem_fp) { + log_error("open_memstream() failed: %m\n"); + close(pipe_fd[0]); + return; + } + + // TODO: Make this asynchronous + while ((ret = read(pipe_fd[0], &buf, sizeof(buf))) > 0) + fwrite(&buf, 1, ret, mem_fp); + + fclose(mem_fp); + + if (mem_size) + nvnc_send_cut_text(self->server, mem_data, mem_size); + + free(mem_data); + close(pipe_fd[0]); + zwlr_data_control_offer_v1_destroy(zwlr_data_control_offer_v1); +} + +static bool check_mime_type(const char *type) +{ + if (strcmp(type, "text/plain;charset=utf-8") == 0) + return true; + return false; +} + +bool hack(struct data_control* self) +{ + if (self->hack_to_workaround_wlroots_offering_us_the_source_selection_we_sent) { + self->hack_to_workaround_wlroots_offering_us_the_source_selection_we_sent = false; + return true; + } + return false; +} + +static void data_control_offer(void* data, + struct zwlr_data_control_offer_v1* zwlr_data_control_offer_v1, + const char* mime_type) +{ + struct data_control* self = data; + + if (self->offer) + return; + if (!check_mime_type(mime_type)) { + return; + } + + self->offer = zwlr_data_control_offer_v1; + self->mime_type = strdup(mime_type); +} + +struct zwlr_data_control_offer_v1_listener data_control_offer_listener = { + data_control_offer +}; + +static void data_control_device_offer(void* data, + struct zwlr_data_control_device_v1* zwlr_data_control_device_v1, + struct zwlr_data_control_offer_v1* id) +{ + struct data_control* self = data; + if (!id) + return; + if (hack(self)) { + zwlr_data_control_offer_v1_destroy(id); + return; + } + + zwlr_data_control_offer_v1_add_listener(id, &data_control_offer_listener, data); +} + +static void data_control_device_selection(void* data, + struct zwlr_data_control_device_v1* zwlr_data_control_device_v1, + struct zwlr_data_control_offer_v1* id) +{ + struct data_control* self = data; + if (id && self->offer == id) { + receive_data(data, id, self->mime_type); + free(self->mime_type); + self->offer = NULL; + self->mime_type = NULL; + } +} + +static void data_control_device_finished(void* data, + struct zwlr_data_control_device_v1* zwlr_data_control_device_v1) +{ + zwlr_data_control_device_v1_destroy(zwlr_data_control_device_v1); +} + +static void data_control_device_primary_selection(void* data, + struct zwlr_data_control_device_v1* zwlr_data_control_device_v1, + struct zwlr_data_control_offer_v1* id) +{ + struct data_control* self = data; + if (id && self->offer == id) { + receive_data(data, id, self->mime_type); + free(self->mime_type); + self->offer = NULL; + self->mime_type = NULL; + return; + } +} + +static struct zwlr_data_control_device_v1_listener data_control_device_listener = { + .data_offer = data_control_device_offer, + .selection = data_control_device_selection, + .finished = data_control_device_finished, + .primary_selection = data_control_device_primary_selection +}; + +static void +data_control_source_send(void* data, + struct zwlr_data_control_source_v1* zwlr_data_control_source_v1, + const char* mime_type, + int32_t fd) +{ + struct data_control* self = data; + char* d = self->cb_data; + size_t len = self->cb_len; + int ret; + + assert(d); + + ret = write(fd, d, len); + + if (ret < len) + log_error("write from clipboard incomplete\n"); + + close(fd); +} + +static void data_control_source_cancelled(void* data, + struct zwlr_data_control_source_v1* zwlr_data_control_source_v1) +{ + struct data_control* self = data; + + if (self->data_control_source == zwlr_data_control_source_v1) { + self->data_control_source = NULL; + free(self->cb_data); + self->cb_data = NULL; + } + zwlr_data_control_source_v1_destroy(zwlr_data_control_source_v1); +} + +struct zwlr_data_control_source_v1_listener data_control_source_listener = { + .send = data_control_source_send, + .cancelled = data_control_source_cancelled +}; + +void data_control_init(struct data_control* self, struct wl_display* wl_display, struct nvnc* server, struct wl_seat* seat) +{ + self->wl_display = wl_display; + self->server = server; + self->data_control_device = zwlr_data_control_manager_v1_get_data_device(self->manager, seat); + zwlr_data_control_device_v1_add_listener(self->data_control_device, &data_control_device_listener, self); + self->data_control_source = NULL; + self->cb_data = NULL; + self->cb_len = 0; + self->hack_to_workaround_wlroots_offering_us_the_source_selection_we_sent = false; +} + +void data_control_destroy(struct data_control* self) +{ + if (self->data_control_source) + zwlr_data_control_source_v1_destroy(self->data_control_source); + free(self->mime_type); + free(self->cb_data); + + zwlr_data_control_manager_v1_destroy(self->manager); +} + +void data_control_to_clipboard(struct data_control* self, const char* text, size_t len) +{ + if (!len) { + log_error("%s called with 0 length\n", __func__); + return; + } + free(self->cb_data); + if (self->data_control_source) + zwlr_data_control_source_v1_destroy(self->data_control_source); + + self->cb_data = malloc(len); + if (!self->cb_data) { + log_error("OOM: %m\n"); + return; + } + + self->data_control_source = zwlr_data_control_manager_v1_create_data_source(self->manager); + if (self->data_control_source == NULL) { + log_error("zwlr_data_control_manager_v1_create_data_source() failed\n"); + free(self->cb_data); + self->cb_data = NULL; + return; + } + + memcpy(self->cb_data, text, len); + self->cb_len = len; + self->hack_to_workaround_wlroots_offering_us_the_source_selection_we_sent = true; + zwlr_data_control_source_v1_add_listener(self->data_control_source, &data_control_source_listener, self); + zwlr_data_control_source_v1_offer(self->data_control_source, "text/plain;charset=utf-8"); + zwlr_data_control_device_v1_set_selection(self->data_control_device, self->data_control_source); + // Calling set_selection and set_primary_selection together causes strange crash in aml. + // zwlr_data_control_device_v1_set_primary_selection(self->data_control_device, self->data_control_source); +} diff --git a/src/main.c b/src/main.c index 2c4dbb5..17aa47f 100644 --- a/src/main.c +++ b/src/main.c @@ -41,6 +41,7 @@ #include "xdg-output-unstable-v1.h" #include "linux-dmabuf-unstable-v1.h" #include "screencopy.h" +#include "data-control.h" #include "strlcpy.h" #include "logging.h" #include "output.h" @@ -84,6 +85,7 @@ struct wayvnc { struct screencopy screencopy; struct pointer pointer_backend; struct keyboard keyboard_backend; + struct data_control data_control; struct aml_handler* wayland_handler; struct aml_signal* signal_handler; @@ -198,6 +200,12 @@ static void registry_add(void* data, struct wl_registry* registry, &zwp_linux_dmabuf_v1_interface, 3); return; } + + if (strcmp(interface, zwlr_data_control_manager_v1_interface.name) == 0) { + self->data_control.manager = wl_registry_bind(registry, id, + &zwlr_data_control_manager_v1_interface, 2); + return; + } } static void registry_remove(void* data, struct wl_registry* registry, @@ -292,6 +300,8 @@ void wayvnc_destroy(struct wayvnc* self) if (self->screencopy.manager) zwlr_screencopy_manager_v1_destroy(self->screencopy.manager); + if (self->data_control.manager) + zwlr_data_control_manager_v1_destroy(self->data_control.manager); wl_registry_destroy(self->registry); wl_display_disconnect(self->display); @@ -444,6 +454,13 @@ static void on_key_event(struct nvnc_client* client, uint32_t symbol, keyboard_feed(&wayvnc->keyboard_backend, symbol, is_pressed); } +static void on_client_cut_text(struct nvnc* server, const char* text, uint32_t len) +{ + struct wayvnc* wayvnc = nvnc_get_userdata(server); + + data_control_to_clipboard(&wayvnc->data_control, text, len); +} + bool on_auth(const char* username, const char* password, void* ud) { struct wayvnc* self = ud; @@ -490,6 +507,8 @@ int init_nvnc(struct wayvnc* self, const char* addr, uint16_t port) if (self->keyboard_backend.virtual_keyboard) nvnc_set_key_fn(self->nvnc, on_key_event); + nvnc_set_cut_text_receive_fn(self->nvnc, on_client_cut_text); + return 0; failure: @@ -938,6 +957,9 @@ int main(int argc, char* argv[]) goto capture_failure; } + data_control_init(&self.data_control, self.display, self.nvnc, + self.selected_seat->wl_seat); + pixman_region_init(&self.current_damage); self.screencopy.overlay_cursor = overlay_cursor; @@ -971,6 +993,8 @@ int main(int argc, char* argv[]) zwp_linux_dmabuf_v1_destroy(zwp_linux_dmabuf); if (self.screencopy.manager) screencopy_destroy(&self.screencopy); + if (self.data_control.manager) + data_control_destroy(&self.data_control); #ifdef ENABLE_SCREENCOPY_DMABUF if (gbm_device) { gbm_device_destroy(gbm_device);