From 40e8d8e52b79309bd05f63f2fea884a97e9bbdcb Mon Sep 17 00:00:00 2001 From: Olivier Fourdan Date: Mon, 9 Mar 2026 16:52:18 +0100 Subject: [PATCH] xwayland: Implement clipboard and primary selection So that it is possible to copy and paste between Xwayland rootful and other Wayland or even X11 clients outside of Xwayland rootful. Signed-off-by: Olivier Fourdan Closes: https://gitlab.freedesktop.org/xorg/xserver/-/issues/1873 Assisted-by: Cursor AI --- hw/xwayland/meson.build | 2 + hw/xwayland/xwayland-input.c | 7 + hw/xwayland/xwayland-screen.c | 1 + hw/xwayland/xwayland-screen.h | 2 + hw/xwayland/xwayland-selection.c | 1165 ++++++++++++++++++++++++++++++ hw/xwayland/xwayland-selection.h | 26 + 6 files changed, 1203 insertions(+) create mode 100644 hw/xwayland/xwayland-selection.c create mode 100644 hw/xwayland/xwayland-selection.h diff --git a/hw/xwayland/meson.build b/hw/xwayland/meson.build index ec7e44b02..cd3a1a661 100644 --- a/hw/xwayland/meson.build +++ b/hw/xwayland/meson.build @@ -2,6 +2,8 @@ srcs = [ 'xwayland.c', 'xwayland-input.c', 'xwayland-input.h', + 'xwayland-selection.c', + 'xwayland-selection.h', 'xwayland-cursor.c', 'xwayland-cursor.h', 'xwayland-drm-lease.h', diff --git a/hw/xwayland/xwayland-input.c b/hw/xwayland/xwayland-input.c index 2ecf1026e..2b808b7ff 100644 --- a/hw/xwayland/xwayland-input.c +++ b/hw/xwayland/xwayland-input.c @@ -42,6 +42,7 @@ #include "xwayland-cursor.h" #include "xwayland-input.h" +#include "xwayland-selection.h" #include "xwayland-window.h" #include "xwayland-screen.h" @@ -634,6 +635,8 @@ pointer_handle_leave(void *data, struct wl_pointer *pointer, if (xwl_screen->rootless) xwl_seat_leave_ptr(xwl_seat, focus_lost); + else + xwl_selection_offer_after_pointer_leave(xwl_seat); } static void @@ -1994,6 +1997,8 @@ create_input_device(struct xwl_screen *xwl_screen, uint32_t id, uint32_t version xorg_list_init(&xwl_seat->touches); xorg_list_init(&xwl_seat->sync_pending); + + xwl_selection_init(xwl_seat); } void @@ -2002,6 +2007,8 @@ xwl_seat_destroy(struct xwl_seat *xwl_seat) struct xwl_touch *xwl_touch, *next_xwl_touch; struct sync_pending *p, *npd; + xwl_selection_fini(xwl_seat); + xorg_list_for_each_entry_safe(xwl_touch, next_xwl_touch, &xwl_seat->touches, link_touch) { xorg_list_del(&xwl_touch->link_touch); diff --git a/hw/xwayland/xwayland-screen.c b/hw/xwayland/xwayland-screen.c index f76fd0fe4..c81633855 100644 --- a/hw/xwayland/xwayland-screen.c +++ b/hw/xwayland/xwayland-screen.c @@ -56,6 +56,7 @@ #include "xwayland-pixmap.h" #include "xwayland-present.h" #include "xwayland-shm.h" +#include "xwayland-selection.h" #ifdef XWL_HAS_EI #include "xwayland-xtest.h" #endif diff --git a/hw/xwayland/xwayland-screen.h b/hw/xwayland/xwayland-screen.h index a809aaf0c..6927c40d5 100644 --- a/hw/xwayland/xwayland-screen.h +++ b/hw/xwayland/xwayland-screen.h @@ -39,6 +39,7 @@ #include "xwayland-glamor.h" #include "xwayland-drm-lease.h" #include "xwayland-dmabuf.h" +#include "xwayland-selection.h" #ifdef XWL_HAS_LIBDECOR #include @@ -121,6 +122,7 @@ struct xwl_screen { struct xdg_system_bell_v1 *system_bell; struct wl_data_device_manager *data_device_manager; struct zwp_primary_selection_device_manager_v1 *primary_selection_manager; + struct xwl_selection *selection_bridge; struct xorg_list drm_lease_devices; struct xorg_list queued_drm_lease_devices; struct xorg_list drm_leases; diff --git a/hw/xwayland/xwayland-selection.c b/hw/xwayland/xwayland-selection.c new file mode 100644 index 000000000..fdb3c091e --- /dev/null +++ b/hw/xwayland/xwayland-selection.c @@ -0,0 +1,1165 @@ +/* + * Copyright © 2026 Red Hat. + * + * 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. + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include + +#include +#include + +#include "xwayland-screen.h" +#include "xwayland-selection.h" +#include "xwayland-input.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include "primary-selection-unstable-v1-client-protocol.h" + +#define TEXT_PLAIN_MIME "text/plain;charset=utf-8" +#define TEXT_PLAIN_LEGACY "text/plain" + +typedef struct { + CARD8 reqType; + CARD8 pad; + CARD16 length; + CARD32 window; + CARD32 selection; + CARD32 time; +} xwl_set_selection_owner_req; + +struct xwl_selection_attrs { + /* Text read from Wayland offer, served to X11 ConvertSelection */ + char *wl_text; + size_t wl_size; + + /* Text from X11 owner (UTF-8), offered to Wayland */ + char *x11_text; + size_t x11_size; + + /* acquisition time for ICCCM TIMESTAMP */ + CARD32 timestamp; + + /* current offer advertises UTF-8 plain text */ + Bool offer_utf8; + + /* current offer advertises text/plain */ + Bool offer_plain; + + uint32_t read_serial; + + Bool read_in_progress; + /* defer set_selection until X11 data cached */ + Bool source_pending_set; + /* skip compositor echo of our set_selection */ + Bool just_set; +}; + +/* Per-read state for SetNotifyFd-based async pipe reads. */ +struct offer_read_closure { + struct xwl_selection *xwl_selection; + struct xwl_selection_attrs *attrs; + Atom selection_atom; + + int fd; + uint32_t serial; + char *data; + size_t size; +}; + +struct xwl_selection { + struct xwl_screen *xwl_screen; + WindowPtr bridge_window; + + /* X11 atoms */ + Atom atom_primary; + Atom atom_clipboard; + Atom atom_utf8_string; + Atom atom_text_plain; + Atom atom_compound_text; + Atom atom_targets; + Atom atom_timestamp; + Atom atom_wayland_clipboard; + Atom atom_wayland_primary; + + /* Wayland clipboard (wl_data_device_manager) */ + struct wl_data_device_manager *data_device_manager; + struct wl_data_device *data_device; + struct wl_data_offer *clipboard_offer; + struct wl_data_source *clipboard_source; + + /* Wayland primary selection */ + struct zwp_primary_selection_device_manager_v1 *primary_selection_manager; + struct zwp_primary_selection_device_v1 *primary_selection_device; + struct zwp_primary_selection_offer_v1 *primary_offer; + struct zwp_primary_selection_source_v1 *primary_source; + + struct xwl_selection_attrs clipboard; + struct xwl_selection_attrs primary; + + /* Serial to use for set_selection: captured when X11 app + * claimed selection (selection_callback) + */ + uint32_t set_selection_serial; + + Bool initialized; +}; + +static void +xwl_selection_clear_data(char **data, size_t *size) +{ + free(*data); + *data = NULL; + *size = 0; +} + +/* Fakes a request from serverClient to set the selection owner */ +static void +xwl_selection_set_owner(struct xwl_selection *xwl_selection, + Atom selection_atom, Window window) +{ + static xwl_set_selection_owner_req req; + void *saved_requestBuffer; + CARD32 saved_req_len; + + if (window != None && !xwl_selection->bridge_window) + return; + + UpdateCurrentTime(); + req.reqType = X_SetSelectionOwner; + req.length = sizeof(req) >> 2; + req.window = window; + req.selection = selection_atom; + req.time = CurrentTime; + + saved_requestBuffer = serverClient->requestBuffer; + saved_req_len = serverClient->req_len; + serverClient->requestBuffer = &req; + serverClient->req_len = sizeof(req) >> 2; + ProcSetSelectionOwner(serverClient); + + serverClient->requestBuffer = saved_requestBuffer; + serverClient->req_len = saved_req_len; +} + +static void +xwl_selection_clear_x11_owner(struct xwl_selection *xwl_selection, + Atom selection_atom) +{ + xwl_selection_set_owner(xwl_selection, selection_atom, None); +} + +static void +xwl_selection_set_bridge_as_owner(struct xwl_selection *xwl_selection, + Atom selection_atom) +{ + xwl_selection_set_owner(xwl_selection, selection_atom, + xwl_selection->bridge_window->drawable.id); +} + +static void +xwl_selection_send_notify(ClientPtr requestor, Window requestor_win, + Atom selection, Atom target, Atom property, + TimeStamp time) +{ + xEvent event; + + memset(&event, 0, sizeof(event)); + event.u.u.type = SelectionNotify; + event.u.selectionNotify.time = time.milliseconds; + event.u.selectionNotify.requestor = requestor_win; + event.u.selectionNotify.selection = selection; + event.u.selectionNotify.target = target; + event.u.selectionNotify.property = property; + + WriteEventsToClient(requestor, 1, &event); +} + +static void +xwl_selection_bridge_request(ScreenPtr screen, ClientPtr requestor, + Window requestor_win, Atom selection, + Atom target, Atom property, TimeStamp time) +{ + struct xwl_screen *xwl_screen = xwl_screen_get(screen); + struct xwl_selection *xwl_selection; + WindowPtr pRequestorWin; + char *data = NULL; + size_t size = 0; + Bool has_text = FALSE; + CARD32 timestamp = 0; + int rc; + + if (!xwl_screen->selection_bridge) { + ErrorF("XWAYLAND: bridge request: no selection_bridge!\n"); + return; + } + + rc = dixLookupWindow(&pRequestorWin, requestor_win, requestor, DixSetAttrAccess); + if (rc != Success) { + ErrorF("XWAYLAND: bridge request: dixLookupWindow failed!\n"); + return; + } + + xwl_selection = xwl_screen->selection_bridge; + + /* ICCCM: if property is None, use target as the property */ + if (property == None) + property = target; + + { + struct xwl_selection_attrs *attrs; + + if (selection == xwl_selection->atom_primary) + attrs = &xwl_selection->primary; + else + attrs = &xwl_selection->clipboard; + + data = attrs->wl_text; + size = attrs->wl_size; + timestamp = attrs->timestamp; + if (!data || size == 0) { + data = attrs->x11_text; + size = attrs->x11_size; + } + } + + has_text = (data != NULL && size > 0); + + if (target == xwl_selection->atom_targets) { + Atom targets[8]; + int n = 0; + + targets[n++] = xwl_selection->atom_targets; + targets[n++] = xwl_selection->atom_timestamp; + if (has_text) { + targets[n++] = xwl_selection->atom_utf8_string; + targets[n++] = xwl_selection->atom_compound_text; + targets[n++] = XA_STRING; + targets[n++] = xwl_selection->atom_text_plain; + } + + if (dixChangeWindowProperty(serverClient, pRequestorWin, property, + XA_ATOM, 32, PropModeReplace, + n, targets, FALSE) != Success) { + xwl_selection_send_notify(requestor, requestor_win, selection, + target, None, time); + return; + } + + xwl_selection_send_notify(requestor, requestor_win, selection, + target, property, time); + return; + } + + if (target == xwl_selection->atom_timestamp) { + if (dixChangeWindowProperty(serverClient, pRequestorWin, property, + XA_INTEGER, 32, PropModeReplace, + 1, ×tamp, FALSE) != Success) { + xwl_selection_send_notify(requestor, requestor_win, selection, + target, None, time); + return; + } + + xwl_selection_send_notify(requestor, requestor_win, selection, + target, property, time); + return; + } + + if (!has_text) { + ErrorF("XWAYLAND: bridge request: no data (target %d)\n", target); + xwl_selection_send_notify(requestor, requestor_win, selection, + target, None, time); + return; + } + + /* + * ICCCM: TEXT target means "convert to your preferred text encoding", + * the reply type should be the actual encoding used, not TEXT itself. + * We always have UTF-8 data, so reply as COMPOUND_TEXT for TEXT requests. + */ + if (target == xwl_selection->atom_utf8_string || + target == xwl_selection->atom_compound_text || + target == xwl_selection->atom_text_plain || + target == XA_STRING) { + Atom reply_type = target; + + if (target == xwl_selection->atom_text_plain) + reply_type = xwl_selection->atom_compound_text; + + if (dixChangeWindowProperty(serverClient, pRequestorWin, property, + reply_type, 8, PropModeReplace, + size, data, FALSE) != Success) { + xwl_selection_send_notify(requestor, requestor_win, selection, + target, None, time); + return; + } + + xwl_selection_send_notify(requestor, requestor_win, selection, + target, property, time); + return; + } + + ErrorF("XWAYLAND: bridge request: unsupported target %d\n", target); + xwl_selection_send_notify(requestor, requestor_win, selection, target, + None, time); +} + +static void +selection_bridge_request_callback(CallbackListPtr *pcbl, void *closure, void *data) +{ + struct xwl_screen *xwl_screen = closure; + SelectionBridgeInfoRec *info = data; + + if (info->screen != xwl_screen->screen) + return; + + xwl_selection_bridge_request(info->screen, info->client, + info->requestor, info->selection, + info->target, info->property, info->time); +} + +/* + * Async offer read via SetNotifyFd. + * + * When data_device_selection or primary_selection_selection receives an offer, + * it creates a pipe, calls wl_data_offer_receive + wl_display_flush, then + * registers the read end with SetNotifyFd. This handler reads data + * incrementally as it arrives, never blocking the X server. When the source + * closes the write end (EOF) or an error occurs, the handler deregisters, + * stores the data (if the serial still matches), and sets the bridge as the + * X11 selection owner. + * + * Ownership is taken here, not in data_device_selection, so that the bridge + * never becomes the X11 owner before it has data to serve. Until the read + * completes, the previous owner (the bridge from an earlier read, or an X11 + * client) continues to handle ConvertSelection requests. + */ +static void +offer_read_fd_handler(int fd, int ready, void *data) +{ + struct offer_read_closure *closure = data; + struct xwl_selection *xwl_selection = closure->xwl_selection; + char buf[4096]; + ssize_t n; + + for (;;) { + n = read(fd, buf, sizeof(buf)); + if (n > 0) { + char *new_data = realloc(closure->data, + closure->size + (size_t) n + 1); + if (!new_data) + break; + closure->data = new_data; + memcpy(closure->data + closure->size, buf, (size_t) n); + closure->size += (size_t) n; + } else if (n == 0) { + break; + } else if (errno == EINTR) { + continue; + } else if (errno == EAGAIN || errno == EWOULDBLOCK) { + return; + } else { + break; + } + } + + SetNotifyFd(fd, NULL, 0, NULL); + close(fd); + + if (closure->data) + closure->data[closure->size] = '\0'; + + UpdateCurrentTime(); + + if (closure->serial == closure->attrs->read_serial) { + struct xwl_selection_attrs *attrs = closure->attrs; + + attrs->read_in_progress = FALSE; + free(attrs->wl_text); + attrs->wl_text = closure->data; + attrs->wl_size = closure->size; + closure->data = NULL; + attrs->timestamp = currentTime.milliseconds; + xwl_selection_set_bridge_as_owner(xwl_selection, + closure->selection_atom); + } + + free(closure->data); + free(closure); +} + +/* Cancel any in-flight async read for the given selection by bumping the + * serial. The still-registered SetNotifyFd handler will see a stale serial + * when it finishes and discard the data. The fd is cleaned up by the handler. + */ +static void +cancel_async_read(struct xwl_selection_attrs *attrs) +{ + if (attrs->read_in_progress) + attrs->read_serial++; +} + +static void +start_async_offer_read(struct xwl_selection *xwl_selection, + struct xwl_selection_attrs *attrs, + void *offer, const char *mime_type, + Atom selection_atom) +{ + struct offer_read_closure *closure; + int pipe_fds[2]; + + if (pipe(pipe_fds) != 0) + return; + + fcntl(pipe_fds[0], F_SETFL, fcntl(pipe_fds[0], F_GETFL) | O_NONBLOCK); + + if (selection_atom == xwl_selection->atom_primary) + zwp_primary_selection_offer_v1_receive(offer, mime_type, pipe_fds[1]); + else + wl_data_offer_receive(offer, mime_type, pipe_fds[1]); + close(pipe_fds[1]); + + wl_display_flush(xwl_selection->xwl_screen->display); + + closure = calloc(1, sizeof(*closure)); + if (!closure) { + close(pipe_fds[0]); + return; + } + + cancel_async_read(attrs); + + closure->xwl_selection = xwl_selection; + closure->attrs = attrs; + closure->selection_atom = selection_atom; + closure->fd = pipe_fds[0]; + closure->serial = ++attrs->read_serial; + attrs->read_in_progress = TRUE; + + SetNotifyFd(pipe_fds[0], offer_read_fd_handler, X_NOTIFY_READ, closure); +} + +/* Pick best text MIME type to request from the current offer. */ +static const char * +pick_text_mime(struct xwl_selection_attrs *attrs) +{ + if (attrs->offer_utf8) + return TEXT_PLAIN_MIME; + if (attrs->offer_plain) + return TEXT_PLAIN_LEGACY; + return NULL; +} + +static void +xwl_data_offer(void *data, struct wl_data_offer *offer, const char *mime_type) +{ + struct xwl_selection *xwl_selection = data; + + if (!mime_type) + return; + if (strcmp(mime_type, TEXT_PLAIN_MIME) == 0) + xwl_selection->clipboard.offer_utf8 = TRUE; + else if (strcmp(mime_type, TEXT_PLAIN_LEGACY) == 0) + xwl_selection->clipboard.offer_plain = TRUE; +} + +static const struct wl_data_offer_listener data_offer_listener = { + .offer = xwl_data_offer, +}; + +static void +xwl_primary_offer(void *data, + struct zwp_primary_selection_offer_v1 *offer, + const char *mime_type) +{ + struct xwl_selection *xwl_selection = data; + + if (!mime_type) + return; + if (strcmp(mime_type, TEXT_PLAIN_MIME) == 0) + xwl_selection->primary.offer_utf8 = TRUE; + else if (strcmp(mime_type, TEXT_PLAIN_LEGACY) == 0) + xwl_selection->primary.offer_plain = TRUE; +} + +static const struct zwp_primary_selection_offer_v1_listener + primary_offer_listener = { + .offer = xwl_primary_offer, +}; + +static void +data_device_data_offer(void *data, + struct wl_data_device *device, + struct wl_data_offer *offer) +{ + struct xwl_selection *xwl_selection = data; + + if (xwl_selection->clipboard_offer) + wl_data_offer_destroy(xwl_selection->clipboard_offer); + + xwl_selection->clipboard_offer = offer; + xwl_selection->clipboard.offer_utf8 = FALSE; + xwl_selection->clipboard.offer_plain = FALSE; + + if (offer) + wl_data_offer_add_listener(offer, &data_offer_listener, xwl_selection); +} + +static void +data_device_selection(void *data, + struct wl_data_device *device, + struct wl_data_offer *offer) +{ + struct xwl_selection *xwl_selection = data; + const char *mime; + + if (xwl_selection->clipboard_offer && + xwl_selection->clipboard_offer != offer) { + wl_data_offer_destroy(xwl_selection->clipboard_offer); + } + + xwl_selection->clipboard_offer = offer; + + /* The compositor echoes our own set_selection back; skip processing. */ + if (xwl_selection->clipboard.just_set) { + xwl_selection->clipboard.just_set = FALSE; + return; + } + + if (offer) { + mime = pick_text_mime(&xwl_selection->clipboard); + if (mime) + start_async_offer_read(xwl_selection, &xwl_selection->clipboard, + offer, mime, xwl_selection->atom_clipboard); + } else { + if (!xwl_selection->clipboard.read_in_progress) + xwl_selection_clear_x11_owner(xwl_selection, + xwl_selection->atom_clipboard); + } +} + +static void +data_device_enter(void *data, struct wl_data_device *device, + uint32_t serial, struct wl_surface *surface, + wl_fixed_t x, wl_fixed_t y, struct wl_data_offer *offer) +{ +} + +static void +data_device_leave(void *data, struct wl_data_device *device) +{ +} + +static void +data_device_motion(void *data, struct wl_data_device *device, + uint32_t time, wl_fixed_t x, wl_fixed_t y) +{ +} + +static void +data_device_drop(void *data, struct wl_data_device *device) +{ +} + +static const struct wl_data_device_listener data_device_listener = { + .data_offer = data_device_data_offer, + .enter = data_device_enter, + .leave = data_device_leave, + .motion = data_device_motion, + .drop = data_device_drop, + .selection = data_device_selection, +}; + +static void +primary_selection_device_data_offer(void *data, + struct zwp_primary_selection_device_v1 *device, + struct zwp_primary_selection_offer_v1 *offer) +{ + struct xwl_selection *xwl_selection = data; + + if (xwl_selection->primary_offer) + zwp_primary_selection_offer_v1_destroy(xwl_selection->primary_offer); + + xwl_selection->primary_offer = offer; + xwl_selection->primary.offer_utf8 = FALSE; + xwl_selection->primary.offer_plain = FALSE; + + if (offer) + zwp_primary_selection_offer_v1_add_listener(offer, &primary_offer_listener, xwl_selection); +} + +static void +primary_selection_selection(void *data, + struct zwp_primary_selection_device_v1 *device, + struct zwp_primary_selection_offer_v1 *offer) +{ + struct xwl_selection *xwl_selection = data; + const char *mime; + + if (xwl_selection->primary_offer && + xwl_selection->primary_offer != offer) { + zwp_primary_selection_offer_v1_destroy(xwl_selection->primary_offer); + } + + xwl_selection->primary_offer = offer; + + /* The compositor echoes our own set_selection back; skip processing. */ + if (xwl_selection->primary.just_set) { + xwl_selection->primary.just_set = FALSE; + return; + } + + if (offer) { + mime = pick_text_mime(&xwl_selection->primary); + if (mime) + start_async_offer_read(xwl_selection, &xwl_selection->primary, + offer, mime, xwl_selection->atom_primary); + } else { + if (!xwl_selection->primary.read_in_progress) + xwl_selection_clear_x11_owner(xwl_selection, + xwl_selection->atom_primary); + } +} + +static const struct zwp_primary_selection_device_v1_listener + primary_selection_device_listener = { + .data_offer = primary_selection_device_data_offer, + .selection = primary_selection_selection, +}; + +static void +data_source_send(void *data, struct wl_data_source *source, + const char *mime_type, int32_t fd) +{ + struct xwl_selection *xwl_selection = data; + char *buf = NULL; + size_t size = 0; + + if (strcmp(mime_type, TEXT_PLAIN_MIME) == 0 || + strcmp(mime_type, TEXT_PLAIN_LEGACY) == 0) { + buf = xwl_selection->clipboard.x11_text; + size = xwl_selection->clipboard.x11_size; + } + + if (buf && size > 0) { + while (size > 0) { + ssize_t n = write(fd, buf, size); + + if (n > 0) { + buf += (size_t) n; + size -= (size_t) n; + } else if (n < 0 && errno == EINTR) { + continue; + } else { + break; + } + } + } + + close(fd); +} + +static void +data_source_cancelled(void *data, struct wl_data_source *source) +{ + struct xwl_selection *xwl_selection = data; + + wl_data_source_destroy(source); + xwl_selection->clipboard_source = NULL; + xwl_selection->clipboard.source_pending_set = FALSE; + xwl_selection_clear_data(&xwl_selection->clipboard.x11_text, + &xwl_selection->clipboard.x11_size); +} + +static const struct wl_data_source_listener data_source_listener = { + .target = NULL, + .send = data_source_send, + .cancelled = data_source_cancelled, +}; + +static void +primary_source_send(void *data, + struct zwp_primary_selection_source_v1 *source, + const char *mime_type, int32_t fd) +{ + struct xwl_selection *xwl_selection = data; + char *buf = xwl_selection->primary.x11_text; + size_t size = xwl_selection->primary.x11_size; + + if ((strcmp(mime_type, TEXT_PLAIN_MIME) == 0 || + strcmp(mime_type, TEXT_PLAIN_LEGACY) == 0) && buf && size > 0) { + while (size > 0) { + ssize_t n = write(fd, buf, size); + + if (n > 0) { + buf += (size_t) n; + size -= (size_t) n; + } else if (n < 0 && errno == EINTR) { + continue; + } else { + break; + } + } + } + close(fd); +} + +static void +primary_source_cancelled(void *data, + struct zwp_primary_selection_source_v1 *source) +{ + struct xwl_selection *xwl_selection = data; + + zwp_primary_selection_source_v1_destroy(source); + xwl_selection->primary_source = NULL; + xwl_selection->primary.source_pending_set = FALSE; + xwl_selection_clear_data(&xwl_selection->primary.x11_text, + &xwl_selection->primary.x11_size); +} + +static const struct zwp_primary_selection_source_v1_listener + primary_source_listener = { + .send = primary_source_send, + .cancelled = primary_source_cancelled, +}; + +static void +selection_set_clipboard_source(struct xwl_selection *xwl_selection) +{ + struct wl_data_source *data_source; + + if (xwl_selection->clipboard_source) { + wl_data_source_destroy(xwl_selection->clipboard_source); + xwl_selection->clipboard_source = NULL; + } + + xwl_selection->clipboard.source_pending_set = FALSE; + + data_source = wl_data_device_manager_create_data_source(xwl_selection->data_device_manager); + wl_data_source_offer(data_source, TEXT_PLAIN_MIME); + wl_data_source_offer(data_source, TEXT_PLAIN_LEGACY); + wl_data_source_add_listener(data_source, &data_source_listener, xwl_selection); + + xwl_selection->clipboard_source = data_source; + xwl_selection->clipboard.source_pending_set = TRUE; +} + +static void +selection_set_primary_source(struct xwl_selection *xwl_selection) +{ + struct zwp_primary_selection_source_v1 *primary_source; + + if (xwl_selection->primary_source) { + zwp_primary_selection_source_v1_destroy(xwl_selection->primary_source); + xwl_selection->primary_source = NULL; + } + + xwl_selection->primary.source_pending_set = FALSE; + + primary_source = zwp_primary_selection_device_manager_v1_create_source(xwl_selection->primary_selection_manager); + zwp_primary_selection_source_v1_offer(primary_source, TEXT_PLAIN_MIME); + zwp_primary_selection_source_v1_offer(primary_source, TEXT_PLAIN_LEGACY); + zwp_primary_selection_source_v1_add_listener(primary_source, &primary_source_listener, xwl_selection); + + xwl_selection->primary_source = primary_source; + xwl_selection->primary.source_pending_set = TRUE; +} + +static void +selection_callback(CallbackListPtr *p, void *data, void *arg) +{ + struct xwl_screen *xwl_screen = data; + SelectionInfoRec *info = arg; + struct xwl_selection *xwl_selection; + + if (!xwl_screen->selection_bridge || info->kind != SelectionSetOwner) + return; + + xwl_selection = xwl_screen->selection_bridge; + xwl_selection->set_selection_serial = xwl_screen->serial; + + if ((info->selection->selection == xwl_selection->atom_clipboard) && + info->client != serverClient) { + selection_set_clipboard_source(xwl_selection); + } + else if ((info->selection->selection == xwl_selection->atom_primary) && + (info->client != serverClient) && + (xwl_selection->primary_selection_device)) { + selection_set_primary_source(xwl_selection); + } +} + +static int +DeliverSelectionRequest(Atom selection, Atom target, Window requestor, + Atom property, TimeStamp time) +{ + Selection *pSel; + xEvent event; + int rc; + + rc = dixLookupSelection(&pSel, selection, serverClient, DixReadAccess); + if (rc != Success || pSel->window == None) + return BadMatch; + if (pSel->client == serverClient || pSel->client->clientGone) + return BadMatch; + + memset(&event, 0, sizeof(xEvent)); + event.u.u.type = SelectionRequest; + event.u.selectionRequest.owner = pSel->window; + event.u.selectionRequest.time = time.milliseconds; + event.u.selectionRequest.requestor = requestor; + event.u.selectionRequest.selection = selection; + event.u.selectionRequest.target = target; + event.u.selectionRequest.property = property; + + WriteEventsToClient(pSel->client, 1, &event); + + return Success; +} + +void +xwl_selection_offer_after_pointer_leave(struct xwl_seat *xwl_seat) +{ + struct xwl_screen *xwl_screen = xwl_seat->xwl_screen; + struct xwl_selection *xwl_selection; + + if (!xwl_screen->selection_bridge) + return; + + xwl_selection = xwl_screen->selection_bridge; + + if (xwl_selection->clipboard.source_pending_set) { + DeliverSelectionRequest(xwl_selection->atom_clipboard, + XA_STRING, + xwl_selection->bridge_window->drawable.id, + xwl_selection->atom_wayland_clipboard, + currentTime); + } + + if (xwl_selection->primary.source_pending_set && + xwl_selection->primary_selection_device) { + DeliverSelectionRequest(xwl_selection->atom_primary, + XA_STRING, + xwl_selection->bridge_window->drawable.id, + xwl_selection->atom_wayland_primary, + currentTime); + } +} + +static char * +latin1_to_utf8(const char *src, size_t src_len, size_t *out_len) +{ + size_t i, n = 0; + char *utf8; + + for (i = 0; i < src_len; i++) { + if ((unsigned char) src[i] < 0x80) + n += 1; + else + n += 2; + } + + utf8 = malloc(n + 1); + if (!utf8) + return NULL; + + n = 0; + for (i = 0; i < src_len; i++) { + unsigned char c = (unsigned char) src[i]; + + if (c < 0x80) { + utf8[n++] = (char) c; + } else { + utf8[n++] = (char) (0xC0 | (c >> 6)); + utf8[n++] = (char) (0x80 | (c & 0x3F)); + } + } + + utf8[n] = '\0'; + *out_len = n; + + return utf8; +} + +static char * +cache_raw_data(const char *src, size_t src_len, size_t *out_len) +{ + size_t alloc_size; + char *buf; + + alloc_size = src_len + 1; + if (alloc_size <= src_len) + return NULL; + + buf = malloc(alloc_size); + if (!buf) + return NULL; + + memcpy(buf, src, src_len); + buf[src_len] = '\0'; + *out_len = src_len; + + return buf; +} + +static void +selection_cache_prop_data(PropertyPtr prop, char **data, size_t *size) +{ + if (prop->size == 0 || prop->data == NULL) + return; + + xwl_selection_clear_data(data, size); + + if (prop->type == XA_STRING) + *data = latin1_to_utf8((const char *) prop->data, prop->size, size); + else + *data = cache_raw_data((const char *) prop->data, prop->size, size); +} + +static void +selection_property_apply_clipboard(struct xwl_selection *xwl_selection, + PropertyPtr prop) +{ + selection_cache_prop_data(prop, &xwl_selection->clipboard.x11_text, + &xwl_selection->clipboard.x11_size); + + if (!xwl_selection->clipboard.source_pending_set) + return; + + wl_data_device_set_selection(xwl_selection->data_device, + xwl_selection->clipboard_source, + xwl_selection->set_selection_serial); + xwl_selection->clipboard.source_pending_set = FALSE; + xwl_selection->clipboard.just_set = TRUE; +} + +static void +selection_property_apply_primary(struct xwl_selection *xwl_selection, + PropertyPtr prop) +{ + selection_cache_prop_data(prop, &xwl_selection->primary.x11_text, + &xwl_selection->primary.x11_size); + + if (!xwl_selection->primary.source_pending_set) + return; + + zwp_primary_selection_device_v1_set_selection(xwl_selection->primary_selection_device, + xwl_selection->primary_source, + xwl_selection->set_selection_serial); + xwl_selection->primary.source_pending_set = FALSE; + xwl_selection->primary.just_set = TRUE; +} + +static void +property_callback(CallbackListPtr *p, void *closure, void *calldata) +{ + PropertyStateRec *rec = calldata; + struct xwl_screen *xwl_screen = closure; + struct xwl_selection *xwl_selection; + PropertyPtr prop; + + if (!xwl_screen->selection_bridge) + return; + + xwl_selection = xwl_screen->selection_bridge; + if (rec->win != xwl_selection->bridge_window) + return; + + if (rec->state != PropertyNewValue) + return; + + prop = rec->prop; + + if ((prop->propertyName != xwl_selection->atom_wayland_clipboard) && + (prop->propertyName != xwl_selection->atom_wayland_primary)) { + return; + } + + if ((prop->propertyName == xwl_selection->atom_wayland_clipboard) && + (xwl_selection->clipboard_source)) { + selection_property_apply_clipboard(xwl_selection, prop); + } + else if ((prop->propertyName == xwl_selection->atom_wayland_primary) && + (xwl_selection->primary_source)) { + selection_property_apply_primary(xwl_selection, prop); + } +} + +static Bool +create_bridge_window(struct xwl_selection *xwl_selection) +{ + ScreenPtr screen = xwl_selection->xwl_screen->screen; + WindowPtr pRoot = screen->root; + WindowPtr pWin; + int mask = CWEventMask; + XID vlist[1]; + int error; + Window wid; + + wid = FakeClientID(serverClient->index); + vlist[0] = PropertyChangeMask; + pWin = CreateWindow(wid, pRoot, + 0, 0, 1, 1, 0, InputOnly, mask, vlist, + 0, serverClient, wVisual(pRoot), &error); + if (pWin == NULL) { + ErrorF("XWAYLAND: Failed to create bridge window: %d\n", error); + return FALSE; + } + + if (!AddResource(pWin->drawable.id, X11_RESTYPE_WINDOW, (void *) pWin)) { + ErrorF("XWAYLAND: Failed to add resource for the bridge window\n"); + screen->DestroyWindow(pWin); + return FALSE; + } + + xwl_selection->bridge_window = pWin; + + return TRUE; +} + +static void +xwl_selection_init_internal(struct xwl_screen *xwl_screen, struct xwl_seat *xwl_seat) +{ + struct xwl_selection *xwl_selection; + + xwl_selection = calloc(1, sizeof(*xwl_selection)); + if (!xwl_selection) + return; + + xwl_selection->xwl_screen = xwl_screen; + xwl_screen->selection_bridge = xwl_selection; + + xwl_selection->atom_primary = MakeAtom("PRIMARY", 7, TRUE); + xwl_selection->atom_clipboard = MakeAtom("CLIPBOARD", 9, TRUE); + xwl_selection->atom_utf8_string = MakeAtom("UTF8_STRING", 11, TRUE); + xwl_selection->atom_text_plain = MakeAtom("TEXT", 4, TRUE); + xwl_selection->atom_compound_text = MakeAtom("COMPOUND_TEXT", 13, TRUE); + xwl_selection->atom_targets = MakeAtom("TARGETS", 7, TRUE); + xwl_selection->atom_timestamp = MakeAtom("TIMESTAMP", 9, TRUE); + xwl_selection->atom_wayland_clipboard = MakeAtom("XWAYLAND_CLIPBOARD", 18, TRUE); + xwl_selection->atom_wayland_primary = MakeAtom("XWAYLAND_PRIMARY", 16, TRUE); + + xwl_selection->data_device_manager = xwl_screen->data_device_manager; + xwl_selection->primary_selection_manager = xwl_screen->primary_selection_manager; + + if (!create_bridge_window(xwl_selection)) { + free(xwl_selection); + xwl_screen->selection_bridge = NULL; + return; + } + + xwl_selection->data_device = + wl_data_device_manager_get_data_device(xwl_selection->data_device_manager, + xwl_seat->seat); + wl_data_device_add_listener(xwl_selection->data_device, + &data_device_listener, xwl_selection); + + if (xwl_selection->primary_selection_manager) { + xwl_selection->primary_selection_device = + zwp_primary_selection_device_manager_v1_get_device(xwl_selection->primary_selection_manager, + xwl_seat->seat); + zwp_primary_selection_device_v1_add_listener(xwl_selection->primary_selection_device, + &primary_selection_device_listener, + xwl_selection); + } + + AddCallback(&SelectionBridgeCallback, selection_bridge_request_callback, xwl_screen); + AddCallback(&SelectionCallback, selection_callback, xwl_screen); + AddCallback(&PropertyStateCallback, property_callback, xwl_screen); + + xwl_selection->initialized = TRUE; +} + +void +xwl_selection_init(struct xwl_seat *xwl_seat) +{ + struct xwl_screen *xwl_screen = xwl_seat->xwl_screen; + + if (xwl_screen->rootless) + return; + + if (xwl_screen->selection_bridge) + return; + + if (!xwl_screen->data_device_manager) + return; + + xwl_selection_init_internal(xwl_screen, xwl_seat); +} + +void +xwl_selection_fini(struct xwl_seat *xwl_seat) +{ + struct xwl_screen *xwl_screen = xwl_seat->xwl_screen; + struct xwl_selection *xwl_selection = xwl_screen->selection_bridge; + + if (!xwl_selection) + return; + + DeleteCallback(&SelectionBridgeCallback, selection_bridge_request_callback, xwl_screen); + DeleteCallback(&SelectionCallback, selection_callback, xwl_screen); + DeleteCallback(&PropertyStateCallback, property_callback, xwl_screen); + + if (xwl_selection->clipboard_offer) + wl_data_offer_destroy(xwl_selection->clipboard_offer); + + if (xwl_selection->clipboard_source) + wl_data_source_destroy(xwl_selection->clipboard_source); + + if (xwl_selection->data_device) + wl_data_device_destroy(xwl_selection->data_device); + + if (xwl_selection->primary_offer) + zwp_primary_selection_offer_v1_destroy(xwl_selection->primary_offer); + + if (xwl_selection->primary_source) + zwp_primary_selection_source_v1_destroy(xwl_selection->primary_source); + + if (xwl_selection->primary_selection_device) + zwp_primary_selection_device_v1_destroy(xwl_selection->primary_selection_device); + + xwl_selection_clear_data(&xwl_selection->clipboard.wl_text, + &xwl_selection->clipboard.wl_size); + xwl_selection_clear_data(&xwl_selection->clipboard.x11_text, + &xwl_selection->clipboard.x11_size); + xwl_selection_clear_data(&xwl_selection->primary.wl_text, + &xwl_selection->primary.wl_size); + xwl_selection_clear_data(&xwl_selection->primary.x11_text, + &xwl_selection->primary.x11_size); + + free(xwl_selection); + xwl_screen->selection_bridge = NULL; +} diff --git a/hw/xwayland/xwayland-selection.h b/hw/xwayland/xwayland-selection.h new file mode 100644 index 000000000..81cf350ae --- /dev/null +++ b/hw/xwayland/xwayland-selection.h @@ -0,0 +1,26 @@ +/* + * Copyright © 2026 Red Hat. + * + * 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. + */ + +#ifndef XWAYLAND_SELECTION_H +#define XWAYLAND_SELECTION_H + +#include "xwayland-types.h" + +void xwl_selection_init(struct xwl_seat *xwl_seat); +void xwl_selection_fini(struct xwl_seat *xwl_seat); +void xwl_selection_offer_after_pointer_leave(struct xwl_seat *xwl_seat); + +#endif /* XWAYLAND_SELECTION_H */