diff --git a/primary-selection-unstable-v1.xml b/primary-selection-unstable-v1.xml
new file mode 100644
index 0000000..e5a39e3
--- /dev/null
+++ b/primary-selection-unstable-v1.xml
@@ -0,0 +1,225 @@
+
+
+
+ Copyright © 2015, 2016 Red Hat
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice (including the next
+ paragraph) shall be included in all copies or substantial portions of the
+ Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+
+
+ This protocol provides the ability to have a primary selection device to
+ match that of the X server. This primary selection is a shortcut to the
+ common clipboard selection, where text just needs to be selected in order
+ to allow copying it elsewhere. The de facto way to perform this action
+ is the middle mouse button, although it is not limited to this one.
+
+ Clients wishing to honor primary selection should create a primary
+ selection source and set it as the selection through
+ wp_primary_selection_device.set_selection whenever the text selection
+ changes. In order to minimize calls in pointer-driven text selection,
+ it should happen only once after the operation finished. Similarly,
+ a NULL source should be set when text is unselected.
+
+ wp_primary_selection_offer objects are first announced through the
+ wp_primary_selection_device.data_offer event. Immediately after this event,
+ the primary data offer will emit wp_primary_selection_offer.offer events
+ to let know of the mime types being offered.
+
+ When the primary selection changes, the client with the keyboard focus
+ will receive wp_primary_selection_device.selection events. Only the client
+ with the keyboard focus will receive such events with a non-NULL
+ wp_primary_selection_offer. Across keyboard focus changes, previously
+ focused clients will receive wp_primary_selection_device.events with a
+ NULL wp_primary_selection_offer.
+
+ In order to request the primary selection data, the client must pass
+ a recent serial pertaining to the press event that is triggering the
+ operation, if the compositor deems the serial valid and recent, the
+ wp_primary_selection_source.send event will happen in the other end
+ to let the transfer begin. The client owning the primary selection
+ should write the requested data, and close the file descriptor
+ immediately.
+
+ If the primary selection owner client disappeared during the transfer,
+ the client reading the data will receive a
+ wp_primary_selection_device.selection event with a NULL
+ wp_primary_selection_offer, the client should take this as a hint
+ to finish the reads related to the no longer existing offer.
+
+ The primary selection owner should be checking for errors during
+ writes, merely cancelling the ongoing transfer if any happened.
+
+
+
+
+ The primary selection device manager is a singleton global object that
+ provides access to the primary selection. It allows to create
+ wp_primary_selection_source objects, as well as retrieving the per-seat
+ wp_primary_selection_device objects.
+
+
+
+
+ Create a new primary selection source.
+
+
+
+
+
+
+ Create a new data device for a given seat.
+
+
+
+
+
+
+
+ Destroy the primary selection device manager.
+
+
+
+
+
+
+
+ Replaces the current selection. The previous owner of the primary
+ selection will receive a wp_primary_selection_source.cancelled event.
+
+ To unset the selection, set the source to NULL.
+
+
+
+
+
+
+
+ Introduces a new wp_primary_selection_offer object that may be used
+ to receive the current primary selection. Immediately following this
+ event, the new wp_primary_selection_offer object will send
+ wp_primary_selection_offer.offer events to describe the offered mime
+ types.
+
+
+
+
+
+
+ The wp_primary_selection_device.selection event is sent to notify the
+ client of a new primary selection. This event is sent after the
+ wp_primary_selection.data_offer event introducing this object, and after
+ the offer has announced its mimetypes through
+ wp_primary_selection_offer.offer.
+
+ The data_offer is valid until a new offer or NULL is received
+ or until the client loses keyboard focus. The client must destroy the
+ previous selection data_offer, if any, upon receiving this event.
+
+
+
+
+
+
+ Destroy the primary selection device.
+
+
+
+
+
+
+ A wp_primary_selection_offer represents an offer to transfer the contents
+ of the primary selection clipboard to the client. Similar to
+ wl_data_offer, the offer also describes the mime types that the data can
+ be converted to and provides the mechanisms for transferring the data
+ directly to the client.
+
+
+
+
+ To transfer the contents of the primary selection clipboard, the client
+ issues this request and indicates the mime type that 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
+ closes its end, at which point the transfer is complete.
+
+
+
+
+
+
+
+ Destroy the primary selection offer.
+
+
+
+
+
+ Sent immediately after creating announcing the
+ wp_primary_selection_offer through
+ wp_primary_selection_device.data_offer. One event is sent per offered
+ mime type.
+
+
+
+
+
+
+
+ The source side of a wp_primary_selection_offer, it provides a way to
+ describe the offered data and respond to requests to transfer the
+ requested contents of the primary selection clipboard.
+
+
+
+
+ This request adds a mime type to the set of mime types advertised to
+ targets. Can be called several times to offer multiple types.
+
+
+
+
+
+
+ Destroy the primary selection source.
+
+
+
+
+
+ Request for the current primary selection contents from the client.
+ Send the specified mime type over the passed file descriptor, then
+ close it.
+
+
+
+
+
+
+
+ This primary selection source is no longer valid. The client should
+ clean up and destroy this primary selection source.
+
+
+
+
diff --git a/primary_selection.c b/primary_selection.c
new file mode 100644
index 0000000..291c52f
--- /dev/null
+++ b/primary_selection.c
@@ -0,0 +1,597 @@
+/* Wayland compositor running on top of an X server.
+
+Copyright (C) 2022 to various contributors.
+
+This file is part of 12to11.
+
+12to11 is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+12to11 is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with 12to11. If not, see . */
+
+#include
+
+#include "compositor.h"
+
+#include "primary-selection-unstable-v1.h"
+
+typedef struct _PDataDevice PDataDevice;
+typedef struct _PDataSource PDataSource;
+typedef struct _PDataOffer PDataOffer;
+
+enum
+ {
+ WasSentOffer = 1,
+ };
+
+struct _PDataDevice
+{
+ /* The seat associated with this data device. */
+ Seat *seat;
+
+ /* The destroy callback associated with that seat. */
+ void *seat_destroy_key;
+
+ /* Some flags. */
+ int flags;
+
+ /* The wl_resource associated with this data device. */
+ struct wl_resource *resource;
+
+ /* The next and last data devices. */
+ PDataDevice *next, *last;
+};
+
+struct _PDataOffer
+{
+ /* The data source associated with this offer. */
+ PDataSource *source;
+
+ /* The resource associated with this offer. */
+ struct wl_resource *resource;
+
+ /* The next and last data offers associated with the data
+ source. */
+ PDataOffer *next, *last;
+};
+
+struct _PDataSource
+{
+ /* The wl_resource associated with this data source. */
+ struct wl_resource *resource;
+
+ /* List of all data offers attached to this source. */
+ PDataOffer offers;
+
+ /* List of all MIME types provided by this source. */
+ XLList *mime_types;
+
+ /* Number of MIME types provided by this source. */
+ int n_mime_types;
+};
+
+/* The global primary selection manager. */
+static struct wl_global *global_primary_selection_device_manager;
+
+/* The data source currently providing the primary selection. */
+static PDataSource *primary_selection;
+
+/* The last time the primary selection changed. */
+static uint32_t last_change_serial;
+
+/* All data devices. */
+static PDataDevice all_devices;
+
+/* Forward declaration. */
+
+static void NoticeChanged (void);
+
+/* Data offer implementation. */
+
+
+static void
+Receive (struct wl_client *client, struct wl_resource *resource,
+ const char *mime_type, int32_t fd)
+{
+ PDataOffer *offer;
+
+ offer = wl_resource_get_user_data (resource);
+
+ if (!offer)
+ {
+ /* This data offer is inert. */
+ close (fd);
+ return;
+ }
+
+ zwp_primary_selection_source_v1_send_send (offer->source->resource,
+ mime_type, fd);
+ close (fd);
+}
+
+static void
+DestroyOffer (struct wl_client *client, struct wl_resource *resource)
+{
+ wl_resource_destroy (resource);
+}
+
+static struct zwp_primary_selection_offer_v1_interface offer_impl =
+ {
+ .receive = Receive,
+ .destroy = DestroyOffer,
+ };
+
+static void
+FreeDataOffer (PDataOffer *offer)
+{
+ /* Mark this offer as invalid by setting the resource's user_data to
+ NULL. */
+ if (offer->resource)
+ wl_resource_set_user_data (offer->resource, NULL);
+
+ /* Unlink the offer. */
+ offer->last->next = offer->next;
+ offer->next->last = offer->last;
+
+ /* Free the offer. */
+ XLFree (offer);
+}
+
+static void
+HandleOfferResourceDestroy (struct wl_resource *resource)
+{
+ PDataOffer *offer;
+
+ offer = wl_resource_get_user_data (resource);
+
+ if (!offer)
+ /* The offer was made inert. */
+ return;
+
+ offer->resource = NULL;
+ FreeDataOffer (offer);
+}
+
+static struct wl_resource *
+AddDataOffer (struct wl_client *client, PDataSource *source)
+{
+ PDataOffer *offer;
+ struct wl_resource *resource;
+
+ resource = wl_resource_create (client,
+ &zwp_primary_selection_offer_v1_interface,
+ /* 0 means to allocate a new resource
+ ID server-side. */
+ 1, 0);
+
+ if (!resource)
+ return NULL;
+
+ offer = XLCalloc (1, sizeof *offer);
+ offer->next = source->offers.next;
+ offer->last = &source->offers;
+
+ source->offers.next->last = offer;
+ source->offers.next = offer;
+
+ offer->resource = resource;
+ offer->source = source;
+
+ wl_resource_set_implementation (resource, &offer_impl,
+ offer, HandleOfferResourceDestroy);
+
+ return resource;
+}
+
+/* Data source implementation. */
+
+
+static Bool
+FindType (PDataSource *source, const char *mime_type)
+{
+ XLList *tem;
+
+ for (tem = source->mime_types; tem; tem = tem->next)
+ {
+ if (!strcmp (tem->data, mime_type))
+ return True;
+ }
+
+ return False;
+}
+
+static void
+Offer (struct wl_client *client, struct wl_resource *resource,
+ const char *mime_type)
+{
+ PDataSource *source;
+
+ source = wl_resource_get_user_data (resource);
+
+ /* If the type was already offered, simply return. */
+ if (FindType (source, mime_type))
+ return;
+
+ /* Otherwise, add the MIME type to the list of types provided by
+ this source. */
+ source->mime_types = XLListPrepend (source->mime_types,
+ XLStrdup (mime_type));
+ source->n_mime_types++;
+}
+
+static void
+DestroySource (struct wl_client *client, struct wl_resource *resource)
+{
+ wl_resource_destroy (resource);
+}
+
+static struct zwp_primary_selection_source_v1_interface source_impl =
+ {
+ .offer = Offer,
+ .destroy = DestroySource,
+ };
+
+static void
+HandleSourceResourceDestroy (struct wl_resource *resource)
+{
+ PDataSource *source;
+ PDataOffer *offer, *last;
+
+ source = wl_resource_get_user_data (resource);
+
+ /* Now free every data offer. */
+ offer = source->offers.next;
+ while (offer != &source->offers)
+ {
+ last = offer;
+ offer = offer->next;
+
+ FreeDataOffer (last);
+ }
+
+ /* And free the MIME types offered by the source. */
+ XLListFree (source->mime_types, XLFree);
+
+ /* If source is the primary selection, clear it and send the change
+ to all clients. */
+ if (source == primary_selection)
+ {
+ primary_selection = NULL;
+ NoticeChanged ();
+ }
+
+ /* And the source itself. */
+ XLFree (source);
+}
+
+
+/* Device implementation. */
+
+
+static void
+UpdateForSingleReference (PDataDevice *reference)
+{
+ struct wl_resource *scratch, *offer;
+ struct wl_client *client;
+ XLList *type;
+
+ if (!reference->seat)
+ /* This data device is inert... */
+ return;
+
+ client = wl_resource_get_client (reference->resource);
+
+ if (!XLSeatIsClientFocused (reference->seat, client))
+ /* The client is not focused. */
+ return;
+
+ scratch = reference->resource;
+
+ if (!primary_selection)
+ {
+ /* There is no primary selection. Send a NULL selection. */
+ zwp_primary_selection_device_v1_send_selection (scratch, NULL);
+ return;
+ }
+
+ /* Otherwise, create a data offer for that client. */
+ offer = AddDataOffer (client, primary_selection);
+
+ if (!offer)
+ /* Allocation of the offer failed. */
+ return;
+
+ /* Introduce the offer to the client. */
+ zwp_primary_selection_device_v1_send_data_offer (scratch, offer);
+
+ /* Send all the offered MIME types. */
+
+ type = primary_selection->mime_types;
+
+ for (; type; type = type->next)
+ zwp_primary_selection_offer_v1_send_offer (offer, type->data);
+
+ /* And tell the client it is the primary selection. */
+ zwp_primary_selection_device_v1_send_selection (scratch, offer);
+
+ /* Set or clear WasSentOffer based on whether or not an offer was
+ sent. */
+ if (offer)
+ reference->flags |= WasSentOffer;
+ else
+ reference->flags &= ~WasSentOffer;
+}
+
+void
+XLPrimarySelectionHandleFocusChange (Seat *seat)
+{
+ PDataDevice *device;
+ struct wl_client *client;
+ struct wl_resource *scratch;
+
+ /* The focus changed. Send NULL data offers to any non-focused data
+ device that was previously sent a valid data offer, and send data
+ offers to any focused data device that was not. */
+
+ device = all_devices.next;
+ while (device != &all_devices)
+ {
+ if (!device->seat)
+ /* Inert device. */
+ goto next;
+
+ scratch = device->resource;
+ client = wl_resource_get_client (scratch);
+
+ if (device->flags & WasSentOffer
+ && !XLSeatIsClientFocused (device->seat, client))
+ {
+ /* The device was previously sent a data offer, but is no
+ longer focused. Send NULL and clear WasSentOffer. */
+ zwp_primary_selection_device_v1_send_selection (scratch,
+ NULL);
+ device->flags &= ~WasSentOffer;
+ }
+ else if (!(device->flags & WasSentOffer)
+ && primary_selection
+ && XLSeatIsClientFocused (device->seat, client))
+ /* The device is now focused, there is a primary selection,
+ and the device was not sent a data offer. Send the missing
+ data offer now. */
+ UpdateForSingleReference (device);
+
+ next:
+ device = device->next;
+ }
+}
+
+static void
+NoticeChanged (void)
+{
+ PDataDevice *device;
+
+ /* Send data offers to each data device. */
+ device = all_devices.next;
+
+ while (device != &all_devices)
+ {
+ UpdateForSingleReference (device);
+ device = device->next;
+ }
+}
+
+static void
+SetSelection (struct wl_client *client, struct wl_resource *resource,
+ struct wl_resource *source_resource, uint32_t serial)
+{
+ struct wl_resource *scratch;
+
+ if (serial < last_change_serial)
+ /* This change is out of date. Do nothing. */
+ return;
+
+ /* Otherwise, set the primary selection. */
+ last_change_serial = serial;
+
+ if (primary_selection)
+ {
+ /* The primary selection already exists. Send the cancelled
+ event and clear the primary selection variable. */
+ scratch = primary_selection->resource;
+ primary_selection = NULL;
+
+ zwp_primary_selection_source_v1_send_cancelled (scratch);
+ }
+
+ if (source_resource)
+ /* Make source_resource the new primary selection. */
+ primary_selection = wl_resource_get_user_data (source_resource);
+
+ /* Now, send the updated primary selection to clients. */
+ NoticeChanged ();
+}
+
+static void
+DestroyDevice (struct wl_client *client, struct wl_resource *resource)
+{
+ wl_resource_destroy (resource);
+}
+
+static struct zwp_primary_selection_device_v1_interface device_impl =
+ {
+ .set_selection = SetSelection,
+ .destroy = DestroyDevice,
+ };
+
+static void
+HandleSeatDestroy (void *data)
+{
+ PDataDevice *device;
+
+ device = data;
+
+ /* The seat destroy listener must be cancelled manually. */
+ XLSeatCancelDestroyListener (device->seat_destroy_key);
+ device->seat_destroy_key = NULL;
+ device->seat = NULL;
+}
+
+static void
+HandleDeviceResourceDestroy (struct wl_resource *resource)
+{
+ PDataDevice *device;
+
+ device = wl_resource_get_user_data (resource);
+
+ /* Cancel the seat destroy listener should it exist. */
+
+ if (device->seat)
+ XLSeatCancelDestroyListener (device->seat_destroy_key);
+
+ /* Unlink the device. */
+
+ device->last->next = device->next;
+ device->next->last = device->last;
+
+ XLFree (device);
+}
+
+/* Device manager implementation. */
+
+
+static void
+CreateSource (struct wl_client *client, struct wl_resource *resource,
+ uint32_t id)
+{
+ PDataSource *source;
+
+ source = XLSafeMalloc (sizeof *source);
+
+ if (!source)
+ {
+ wl_resource_post_no_memory (resource);
+ return;
+ }
+
+ memset (source, 0, sizeof *source);
+ source->resource
+ = wl_resource_create (client, &zwp_primary_selection_source_v1_interface,
+ wl_resource_get_version (resource), id);
+
+ if (!source->resource)
+ {
+ XLFree (source);
+ wl_resource_post_no_memory (resource);
+ return;
+ }
+
+ source->offers.next = &source->offers;
+ source->offers.last = &source->offers;
+
+ wl_resource_set_implementation (source->resource, &source_impl,
+ source, HandleSourceResourceDestroy);
+}
+
+static void
+GetDevice (struct wl_client *client, struct wl_resource *resource,
+ uint32_t id, struct wl_resource *seat)
+{
+ PDataDevice *device;
+
+ device = XLSafeMalloc (sizeof *device);
+
+ if (!device)
+ {
+ wl_resource_post_no_memory (resource);
+ return;
+ }
+
+ memset (device, 0, sizeof *device);
+ device->resource
+ = wl_resource_create (client,
+ &zwp_primary_selection_device_v1_interface,
+ wl_resource_get_version (resource), id);
+
+ if (!device->resource)
+ {
+ XLFree (device);
+ wl_resource_post_no_memory (resource);
+ return;
+ }
+
+ device->seat = wl_resource_get_user_data (seat);
+
+ if (XLSeatIsInert (device->seat))
+ device->seat = NULL;
+ else
+ device->seat_destroy_key
+ = XLSeatRunOnDestroy (device->seat, HandleSeatDestroy,
+ device);
+
+ /* Link the device into the chain of all devices. */
+ device->next = all_devices.next;
+ device->last = &all_devices;
+ all_devices.next->last = device;
+ all_devices.next = device;
+
+ wl_resource_set_implementation (device->resource, &device_impl,
+ device, HandleDeviceResourceDestroy);
+
+ /* If the primary selection is set and the client is focused, send
+ the data offer now. */
+ if (primary_selection && device->seat
+ && XLSeatIsClientFocused (device->seat, client))
+ UpdateForSingleReference (device);
+}
+
+static void
+Destroy (struct wl_client *client, struct wl_resource *resource)
+{
+ wl_resource_destroy (resource);
+}
+
+static struct zwp_primary_selection_device_manager_v1_interface manager_impl =
+ {
+ .create_source = CreateSource,
+ .get_device = GetDevice,
+ .destroy = Destroy,
+ };
+
+static void
+HandleBind (struct wl_client *client, void *data, uint32_t version,
+ uint32_t id)
+{
+ struct wl_resource *resource;
+
+ resource
+ = wl_resource_create (client,
+ &zwp_primary_selection_device_manager_v1_interface,
+ version, id);
+
+ if (!resource)
+ {
+ wl_client_post_no_memory (client);
+ return;
+ }
+
+ wl_resource_set_implementation (resource, &manager_impl, NULL, NULL);
+}
+
+void
+XLInitPrimarySelection (void)
+{
+ global_primary_selection_device_manager
+ = wl_global_create (compositor.wl_display,
+ &zwp_primary_selection_device_manager_v1_interface,
+ 1, NULL, HandleBind);
+ all_devices.next = &all_devices;
+ all_devices.last = &all_devices;
+}