esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
commit 2e672b43b33d8158e7c6876b887480cefd2bff12 parent 10a81c56c8d241cb8a20acdd0a21ecfffd068f61 Author: Marc Coquand <marc@coquand.email> Date: Wed, 25 Feb 2026 16:21:12 +0100 Test wayland primary selection Diffstat:
| M | Makefile | | | 21 | +++++++++++++++++---- |
| M | main.c | | | 5 | ++++- |
| A | primary-selection-unstable-v1-client-protocol.h | | | 582 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | primary-selection-unstable-v1-protocol.c | | | 116 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | unix_utils.h | | | 5 | +++++ |
| A | wayland_primary.c | | | 260 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | wayland_primary.h | | | 17 | +++++++++++++++++ |
7 files changed, 1001 insertions(+), 5 deletions(-)
diff --git a/Makefile b/Makefile
@@ -5,15 +5,28 @@ SANITIZE = -fsanitize=address,undefined
SDL3_CFLAGS = $(shell pkg-config --cflags sdl3)
SDL3_LIBS = $(shell pkg-config --libs sdl3)
+WAYLAND_SCANNER ?= wayland-scanner
+PS_XML = /usr/share/wayland-protocols/unstable/primary-selection/primary-selection-unstable-v1.xml
+PS_HDR = primary-selection-unstable-v1-client-protocol.h
+PS_SRC = primary-selection-unstable-v1-protocol.c
+
.PHONY: all test clean
all: esc
+$(PS_HDR): $(PS_XML)
+ $(WAYLAND_SCANNER) client-header $< $@
+
+$(PS_SRC): $(PS_XML)
+ $(WAYLAND_SCANNER) private-code $< $@
+
esc: main.c unix_utils.c unix_utils.h editor.c editor.h \
- strbuf.c strbuf.h fuse_ipc.c fuse_ipc.h renderer.c renderer.h
+ strbuf.c strbuf.h fuse_ipc.c fuse_ipc.h renderer.c renderer.h \
+ wayland_primary.c wayland_primary.h $(PS_HDR) $(PS_SRC)
$(CC) $(CFLAGS) $(CFLAGS_OPT) main.c unix_utils.c editor.c strbuf.c \
- fuse_ipc.c renderer.c -o esc \
- $(shell pkg-config --cflags --libs sdl3 sdl3-ttf fuse)
+ fuse_ipc.c renderer.c wayland_primary.c $(PS_SRC) -o esc \
+ $(shell pkg-config --cflags --libs sdl3 sdl3-ttf fuse) \
+ $(shell pkg-config --libs wayland-client)
test: tests/test_strbuf tests/test_editor
@echo "--- running test_strbuf ---"
@@ -32,4 +45,4 @@ tests/test_editor: tests/test_editor.c tests/test_harness.h \
$(SDL3_LIBS) -o tests/test_editor
clean:
- rm -f esc tests/test_strbuf tests/test_editor
+ rm -f esc tests/test_strbuf tests/test_editor $(PS_HDR) $(PS_SRC)
diff --git a/main.c b/main.c
@@ -2,6 +2,7 @@
#include "fuse_ipc.h"
#include "renderer.h"
#include "unix_utils.h"
+#include "wayland_primary.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_clipboard.h>
#include <SDL3/SDL_events.h>
@@ -277,7 +278,7 @@ void handle_events(Editor *ed, SDL_Renderer *renderer, float *scroll_x,
if (editor_has_selection(ed)) {
char *sel = editor_get_selection(ed);
if (sel) {
- SDL_SetPrimarySelectionText(sel);
+ wayland_primary_set(sel);
free(sel);
}
}
@@ -318,6 +319,7 @@ int main(int argc, char *argv[]) {
float line_height = (float)TTF_GetFontHeight(font) * 1.5;
float cursor_height = (float)TTF_GetFontHeight(font);
Editor *ed = editor_create(char_width, line_height);
+ wayland_primary_init(window);
if (argc > 1) {
editor_load_file(ed, argv[1]);
editor_parse_ansi_codes(ed);
@@ -338,6 +340,7 @@ int main(int argc, char *argv[]) {
while (running) {
handle_events(ed, renderer, &scroll_x, &scroll_y, line_height,
&running, &is_dragging);
+ wayland_primary_dispatch();
ctx.scroll_x = scroll_x;
ctx.scroll_y = scroll_y;
diff --git a/primary-selection-unstable-v1-client-protocol.h b/primary-selection-unstable-v1-client-protocol.h
@@ -0,0 +1,582 @@
+/* Generated by wayland-scanner 1.23.1 */
+
+#ifndef WP_PRIMARY_SELECTION_UNSTABLE_V1_CLIENT_PROTOCOL_H
+#define WP_PRIMARY_SELECTION_UNSTABLE_V1_CLIENT_PROTOCOL_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_wp_primary_selection_unstable_v1 The wp_primary_selection_unstable_v1 protocol
+ * Primary selection protocol
+ *
+ * @section page_desc_wp_primary_selection_unstable_v1 Description
+ *
+ * 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.
+ *
+ * @section page_ifaces_wp_primary_selection_unstable_v1 Interfaces
+ * - @subpage page_iface_zwp_primary_selection_device_manager_v1 - X primary selection emulation
+ * - @subpage page_iface_zwp_primary_selection_device_v1 -
+ * - @subpage page_iface_zwp_primary_selection_offer_v1 - offer to transfer primary selection contents
+ * - @subpage page_iface_zwp_primary_selection_source_v1 - offer to replace the contents of the primary selection
+ * @section page_copyright_wp_primary_selection_unstable_v1 Copyright
+ * <pre>
+ *
+ * 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.
+ * </pre>
+ */
+struct wl_seat;
+struct zwp_primary_selection_device_manager_v1;
+struct zwp_primary_selection_device_v1;
+struct zwp_primary_selection_offer_v1;
+struct zwp_primary_selection_source_v1;
+
+#ifndef ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_INTERFACE
+#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_INTERFACE
+/**
+ * @page page_iface_zwp_primary_selection_device_manager_v1 zwp_primary_selection_device_manager_v1
+ * @section page_iface_zwp_primary_selection_device_manager_v1_desc Description
+ *
+ * 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.
+ * @section page_iface_zwp_primary_selection_device_manager_v1_api API
+ * See @ref iface_zwp_primary_selection_device_manager_v1.
+ */
+/**
+ * @defgroup iface_zwp_primary_selection_device_manager_v1 The zwp_primary_selection_device_manager_v1 interface
+ *
+ * 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.
+ */
+extern const struct wl_interface zwp_primary_selection_device_manager_v1_interface;
+#endif
+#ifndef ZWP_PRIMARY_SELECTION_DEVICE_V1_INTERFACE
+#define ZWP_PRIMARY_SELECTION_DEVICE_V1_INTERFACE
+/**
+ * @page page_iface_zwp_primary_selection_device_v1 zwp_primary_selection_device_v1
+ * @section page_iface_zwp_primary_selection_device_v1_api API
+ * See @ref iface_zwp_primary_selection_device_v1.
+ */
+/**
+ * @defgroup iface_zwp_primary_selection_device_v1 The zwp_primary_selection_device_v1 interface
+ */
+extern const struct wl_interface zwp_primary_selection_device_v1_interface;
+#endif
+#ifndef ZWP_PRIMARY_SELECTION_OFFER_V1_INTERFACE
+#define ZWP_PRIMARY_SELECTION_OFFER_V1_INTERFACE
+/**
+ * @page page_iface_zwp_primary_selection_offer_v1 zwp_primary_selection_offer_v1
+ * @section page_iface_zwp_primary_selection_offer_v1_desc Description
+ *
+ * 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.
+ * @section page_iface_zwp_primary_selection_offer_v1_api API
+ * See @ref iface_zwp_primary_selection_offer_v1.
+ */
+/**
+ * @defgroup iface_zwp_primary_selection_offer_v1 The zwp_primary_selection_offer_v1 interface
+ *
+ * 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.
+ */
+extern const struct wl_interface zwp_primary_selection_offer_v1_interface;
+#endif
+#ifndef ZWP_PRIMARY_SELECTION_SOURCE_V1_INTERFACE
+#define ZWP_PRIMARY_SELECTION_SOURCE_V1_INTERFACE
+/**
+ * @page page_iface_zwp_primary_selection_source_v1 zwp_primary_selection_source_v1
+ * @section page_iface_zwp_primary_selection_source_v1_desc Description
+ *
+ * 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.
+ * @section page_iface_zwp_primary_selection_source_v1_api API
+ * See @ref iface_zwp_primary_selection_source_v1.
+ */
+/**
+ * @defgroup iface_zwp_primary_selection_source_v1 The zwp_primary_selection_source_v1 interface
+ *
+ * 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.
+ */
+extern const struct wl_interface zwp_primary_selection_source_v1_interface;
+#endif
+
+#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_CREATE_SOURCE 0
+#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_GET_DEVICE 1
+#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_DESTROY 2
+
+
+/**
+ * @ingroup iface_zwp_primary_selection_device_manager_v1
+ */
+#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_CREATE_SOURCE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_primary_selection_device_manager_v1
+ */
+#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_GET_DEVICE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_primary_selection_device_manager_v1
+ */
+#define ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_DESTROY_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_primary_selection_device_manager_v1 */
+static inline void
+zwp_primary_selection_device_manager_v1_set_user_data(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) zwp_primary_selection_device_manager_v1, user_data);
+}
+
+/** @ingroup iface_zwp_primary_selection_device_manager_v1 */
+static inline void *
+zwp_primary_selection_device_manager_v1_get_user_data(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) zwp_primary_selection_device_manager_v1);
+}
+
+static inline uint32_t
+zwp_primary_selection_device_manager_v1_get_version(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1)
+{
+ return wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_device_manager_v1);
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_device_manager_v1
+ *
+ * Create a new primary selection source.
+ */
+static inline struct zwp_primary_selection_source_v1 *
+zwp_primary_selection_device_manager_v1_create_source(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) zwp_primary_selection_device_manager_v1,
+ ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_CREATE_SOURCE, &zwp_primary_selection_source_v1_interface, wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_device_manager_v1), 0, NULL);
+
+ return (struct zwp_primary_selection_source_v1 *) id;
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_device_manager_v1
+ *
+ * Create a new data device for a given seat.
+ */
+static inline struct zwp_primary_selection_device_v1 *
+zwp_primary_selection_device_manager_v1_get_device(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1, struct wl_seat *seat)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) zwp_primary_selection_device_manager_v1,
+ ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_GET_DEVICE, &zwp_primary_selection_device_v1_interface, wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_device_manager_v1), 0, NULL, seat);
+
+ return (struct zwp_primary_selection_device_v1 *) id;
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_device_manager_v1
+ *
+ * Destroy the primary selection device manager.
+ */
+static inline void
+zwp_primary_selection_device_manager_v1_destroy(struct zwp_primary_selection_device_manager_v1 *zwp_primary_selection_device_manager_v1)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_primary_selection_device_manager_v1,
+ ZWP_PRIMARY_SELECTION_DEVICE_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_device_manager_v1), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_device_v1
+ * @struct zwp_primary_selection_device_v1_listener
+ */
+struct zwp_primary_selection_device_v1_listener {
+ /**
+ * introduce a new wp_primary_selection_offer
+ *
+ * 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.
+ */
+ void (*data_offer)(void *data,
+ struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1,
+ struct zwp_primary_selection_offer_v1 *offer);
+ /**
+ * advertise a new primary selection
+ *
+ * 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.
+ */
+ void (*selection)(void *data,
+ struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1,
+ struct zwp_primary_selection_offer_v1 *id);
+};
+
+/**
+ * @ingroup iface_zwp_primary_selection_device_v1
+ */
+static inline int
+zwp_primary_selection_device_v1_add_listener(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1,
+ const struct zwp_primary_selection_device_v1_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) zwp_primary_selection_device_v1,
+ (void (**)(void)) listener, data);
+}
+
+#define ZWP_PRIMARY_SELECTION_DEVICE_V1_SET_SELECTION 0
+#define ZWP_PRIMARY_SELECTION_DEVICE_V1_DESTROY 1
+
+/**
+ * @ingroup iface_zwp_primary_selection_device_v1
+ */
+#define ZWP_PRIMARY_SELECTION_DEVICE_V1_DATA_OFFER_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_primary_selection_device_v1
+ */
+#define ZWP_PRIMARY_SELECTION_DEVICE_V1_SELECTION_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_zwp_primary_selection_device_v1
+ */
+#define ZWP_PRIMARY_SELECTION_DEVICE_V1_SET_SELECTION_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_primary_selection_device_v1
+ */
+#define ZWP_PRIMARY_SELECTION_DEVICE_V1_DESTROY_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_primary_selection_device_v1 */
+static inline void
+zwp_primary_selection_device_v1_set_user_data(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) zwp_primary_selection_device_v1, user_data);
+}
+
+/** @ingroup iface_zwp_primary_selection_device_v1 */
+static inline void *
+zwp_primary_selection_device_v1_get_user_data(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) zwp_primary_selection_device_v1);
+}
+
+static inline uint32_t
+zwp_primary_selection_device_v1_get_version(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1)
+{
+ return wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_device_v1);
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_device_v1
+ *
+ * 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.
+ */
+static inline void
+zwp_primary_selection_device_v1_set_selection(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1, struct zwp_primary_selection_source_v1 *source, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_primary_selection_device_v1,
+ ZWP_PRIMARY_SELECTION_DEVICE_V1_SET_SELECTION, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_device_v1), 0, source, serial);
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_device_v1
+ *
+ * Destroy the primary selection device.
+ */
+static inline void
+zwp_primary_selection_device_v1_destroy(struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_primary_selection_device_v1,
+ ZWP_PRIMARY_SELECTION_DEVICE_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_device_v1), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_offer_v1
+ * @struct zwp_primary_selection_offer_v1_listener
+ */
+struct zwp_primary_selection_offer_v1_listener {
+ /**
+ * advertise offered mime type
+ *
+ * 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.
+ */
+ void (*offer)(void *data,
+ struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1,
+ const char *mime_type);
+};
+
+/**
+ * @ingroup iface_zwp_primary_selection_offer_v1
+ */
+static inline int
+zwp_primary_selection_offer_v1_add_listener(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1,
+ const struct zwp_primary_selection_offer_v1_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) zwp_primary_selection_offer_v1,
+ (void (**)(void)) listener, data);
+}
+
+#define ZWP_PRIMARY_SELECTION_OFFER_V1_RECEIVE 0
+#define ZWP_PRIMARY_SELECTION_OFFER_V1_DESTROY 1
+
+/**
+ * @ingroup iface_zwp_primary_selection_offer_v1
+ */
+#define ZWP_PRIMARY_SELECTION_OFFER_V1_OFFER_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_zwp_primary_selection_offer_v1
+ */
+#define ZWP_PRIMARY_SELECTION_OFFER_V1_RECEIVE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_primary_selection_offer_v1
+ */
+#define ZWP_PRIMARY_SELECTION_OFFER_V1_DESTROY_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_primary_selection_offer_v1 */
+static inline void
+zwp_primary_selection_offer_v1_set_user_data(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) zwp_primary_selection_offer_v1, user_data);
+}
+
+/** @ingroup iface_zwp_primary_selection_offer_v1 */
+static inline void *
+zwp_primary_selection_offer_v1_get_user_data(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) zwp_primary_selection_offer_v1);
+}
+
+static inline uint32_t
+zwp_primary_selection_offer_v1_get_version(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1)
+{
+ return wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_offer_v1);
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_offer_v1
+ *
+ * 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.
+ */
+static inline void
+zwp_primary_selection_offer_v1_receive(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, const char *mime_type, int32_t fd)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_primary_selection_offer_v1,
+ ZWP_PRIMARY_SELECTION_OFFER_V1_RECEIVE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_offer_v1), 0, mime_type, fd);
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_offer_v1
+ *
+ * Destroy the primary selection offer.
+ */
+static inline void
+zwp_primary_selection_offer_v1_destroy(struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_primary_selection_offer_v1,
+ ZWP_PRIMARY_SELECTION_OFFER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_offer_v1), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_source_v1
+ * @struct zwp_primary_selection_source_v1_listener
+ */
+struct zwp_primary_selection_source_v1_listener {
+ /**
+ * send the primary selection contents
+ *
+ * Request for the current primary selection contents from the
+ * client. Send the specified mime type over the passed file
+ * descriptor, then close it.
+ */
+ void (*send)(void *data,
+ struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1,
+ const char *mime_type,
+ int32_t fd);
+ /**
+ * request for primary selection contents was canceled
+ *
+ * This primary selection source is no longer valid. The client
+ * should clean up and destroy this primary selection source.
+ */
+ void (*cancelled)(void *data,
+ struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1);
+};
+
+/**
+ * @ingroup iface_zwp_primary_selection_source_v1
+ */
+static inline int
+zwp_primary_selection_source_v1_add_listener(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1,
+ const struct zwp_primary_selection_source_v1_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) zwp_primary_selection_source_v1,
+ (void (**)(void)) listener, data);
+}
+
+#define ZWP_PRIMARY_SELECTION_SOURCE_V1_OFFER 0
+#define ZWP_PRIMARY_SELECTION_SOURCE_V1_DESTROY 1
+
+/**
+ * @ingroup iface_zwp_primary_selection_source_v1
+ */
+#define ZWP_PRIMARY_SELECTION_SOURCE_V1_SEND_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_primary_selection_source_v1
+ */
+#define ZWP_PRIMARY_SELECTION_SOURCE_V1_CANCELLED_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_zwp_primary_selection_source_v1
+ */
+#define ZWP_PRIMARY_SELECTION_SOURCE_V1_OFFER_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_primary_selection_source_v1
+ */
+#define ZWP_PRIMARY_SELECTION_SOURCE_V1_DESTROY_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_primary_selection_source_v1 */
+static inline void
+zwp_primary_selection_source_v1_set_user_data(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) zwp_primary_selection_source_v1, user_data);
+}
+
+/** @ingroup iface_zwp_primary_selection_source_v1 */
+static inline void *
+zwp_primary_selection_source_v1_get_user_data(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) zwp_primary_selection_source_v1);
+}
+
+static inline uint32_t
+zwp_primary_selection_source_v1_get_version(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1)
+{
+ return wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_source_v1);
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_source_v1
+ *
+ * This request adds a mime type to the set of mime types advertised to
+ * targets. Can be called several times to offer multiple types.
+ */
+static inline void
+zwp_primary_selection_source_v1_offer(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1, const char *mime_type)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_primary_selection_source_v1,
+ ZWP_PRIMARY_SELECTION_SOURCE_V1_OFFER, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_source_v1), 0, mime_type);
+}
+
+/**
+ * @ingroup iface_zwp_primary_selection_source_v1
+ *
+ * Destroy the primary selection source.
+ */
+static inline void
+zwp_primary_selection_source_v1_destroy(struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_primary_selection_source_v1,
+ ZWP_PRIMARY_SELECTION_SOURCE_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_primary_selection_source_v1), WL_MARSHAL_FLAG_DESTROY);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/primary-selection-unstable-v1-protocol.c b/primary-selection-unstable-v1-protocol.c
@@ -0,0 +1,116 @@
+/* Generated by wayland-scanner 1.23.1 */
+
+/*
+ * 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.
+ */
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include "wayland-util.h"
+
+#ifndef __has_attribute
+# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
+#endif
+
+#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
+#define WL_PRIVATE __attribute__ ((visibility("hidden")))
+#else
+#define WL_PRIVATE
+#endif
+
+extern const struct wl_interface wl_seat_interface;
+extern const struct wl_interface zwp_primary_selection_device_v1_interface;
+extern const struct wl_interface zwp_primary_selection_offer_v1_interface;
+extern const struct wl_interface zwp_primary_selection_source_v1_interface;
+
+static const struct wl_interface *wp_primary_selection_unstable_v1_types[] = {
+ NULL,
+ NULL,
+ &zwp_primary_selection_source_v1_interface,
+ &zwp_primary_selection_device_v1_interface,
+ &wl_seat_interface,
+ &zwp_primary_selection_source_v1_interface,
+ NULL,
+ &zwp_primary_selection_offer_v1_interface,
+ &zwp_primary_selection_offer_v1_interface,
+};
+
+static const struct wl_message zwp_primary_selection_device_manager_v1_requests[] = {
+ { "create_source", "n", wp_primary_selection_unstable_v1_types + 2 },
+ { "get_device", "no", wp_primary_selection_unstable_v1_types + 3 },
+ { "destroy", "", wp_primary_selection_unstable_v1_types + 0 },
+};
+
+WL_PRIVATE const struct wl_interface zwp_primary_selection_device_manager_v1_interface = {
+ "zwp_primary_selection_device_manager_v1", 1,
+ 3, zwp_primary_selection_device_manager_v1_requests,
+ 0, NULL,
+};
+
+static const struct wl_message zwp_primary_selection_device_v1_requests[] = {
+ { "set_selection", "?ou", wp_primary_selection_unstable_v1_types + 5 },
+ { "destroy", "", wp_primary_selection_unstable_v1_types + 0 },
+};
+
+static const struct wl_message zwp_primary_selection_device_v1_events[] = {
+ { "data_offer", "n", wp_primary_selection_unstable_v1_types + 7 },
+ { "selection", "?o", wp_primary_selection_unstable_v1_types + 8 },
+};
+
+WL_PRIVATE const struct wl_interface zwp_primary_selection_device_v1_interface = {
+ "zwp_primary_selection_device_v1", 1,
+ 2, zwp_primary_selection_device_v1_requests,
+ 2, zwp_primary_selection_device_v1_events,
+};
+
+static const struct wl_message zwp_primary_selection_offer_v1_requests[] = {
+ { "receive", "sh", wp_primary_selection_unstable_v1_types + 0 },
+ { "destroy", "", wp_primary_selection_unstable_v1_types + 0 },
+};
+
+static const struct wl_message zwp_primary_selection_offer_v1_events[] = {
+ { "offer", "s", wp_primary_selection_unstable_v1_types + 0 },
+};
+
+WL_PRIVATE const struct wl_interface zwp_primary_selection_offer_v1_interface = {
+ "zwp_primary_selection_offer_v1", 1,
+ 2, zwp_primary_selection_offer_v1_requests,
+ 1, zwp_primary_selection_offer_v1_events,
+};
+
+static const struct wl_message zwp_primary_selection_source_v1_requests[] = {
+ { "offer", "s", wp_primary_selection_unstable_v1_types + 0 },
+ { "destroy", "", wp_primary_selection_unstable_v1_types + 0 },
+};
+
+static const struct wl_message zwp_primary_selection_source_v1_events[] = {
+ { "send", "sh", wp_primary_selection_unstable_v1_types + 0 },
+ { "cancelled", "", wp_primary_selection_unstable_v1_types + 0 },
+};
+
+WL_PRIVATE const struct wl_interface zwp_primary_selection_source_v1_interface = {
+ "zwp_primary_selection_source_v1", 1,
+ 2, zwp_primary_selection_source_v1_requests,
+ 2, zwp_primary_selection_source_v1_events,
+};
+
diff --git a/unix_utils.h b/unix_utils.h
@@ -11,4 +11,9 @@ char *unix_run_command(const char *command, const char *input);
// Ignores output
void unix_exec_command(const char *command, const char *input);
+
+// Sets the Wayland primary selection via wl-copy --primary.
+// Text is piped exactly (no trailing newline added).
+// Skips re-spawning if the text hasn't changed.
+void unix_set_primary_selection(const char *text);
#endif
diff --git a/wayland_primary.c b/wayland_primary.c
@@ -0,0 +1,260 @@
+#include "wayland_primary.h"
+#include "primary-selection-unstable-v1-client-protocol.h"
+#include <SDL3/SDL.h>
+#include <wayland-client.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static struct wl_display *s_display;
+static struct wl_registry *s_registry;
+static struct wl_seat *s_seat;
+static struct wl_pointer *s_pointer;
+static struct wl_keyboard *s_keyboard;
+static struct zwp_primary_selection_device_manager_v1 *s_manager;
+static struct zwp_primary_selection_device_v1 *s_device;
+static struct zwp_primary_selection_source_v1 *s_source;
+static struct zwp_primary_selection_offer_v1 *s_pending_offer;
+
+static uint32_t s_last_serial;
+static char *s_current_text;
+
+/* ── source events ─────────────────────────────────────────────────────── */
+
+static void source_send(void *data,
+ struct zwp_primary_selection_source_v1 *source,
+ const char *mime_type, int32_t fd)
+{
+ (void)data; (void)source; (void)mime_type;
+ if (s_current_text)
+ write(fd, s_current_text, strlen(s_current_text));
+ close(fd);
+}
+
+static void source_cancelled(void *data,
+ struct zwp_primary_selection_source_v1 *source)
+{
+ (void)data;
+ zwp_primary_selection_source_v1_destroy(source);
+ if (s_source == source)
+ s_source = NULL;
+ free(s_current_text);
+ s_current_text = NULL;
+}
+
+static const struct zwp_primary_selection_source_v1_listener s_source_listener = {
+ .send = source_send,
+ .cancelled = source_cancelled,
+};
+
+/* ── device events ──────────────────────────────────────────────────────── */
+
+static void device_data_offer(void *data,
+ struct zwp_primary_selection_device_v1 *device,
+ struct zwp_primary_selection_offer_v1 *offer)
+{
+ (void)data; (void)device;
+ if (s_pending_offer)
+ zwp_primary_selection_offer_v1_destroy(s_pending_offer);
+ s_pending_offer = offer;
+}
+
+static void device_selection(void *data,
+ struct zwp_primary_selection_device_v1 *device,
+ struct zwp_primary_selection_offer_v1 *id)
+{
+ (void)data; (void)device; (void)id;
+ if (s_pending_offer) {
+ zwp_primary_selection_offer_v1_destroy(s_pending_offer);
+ s_pending_offer = NULL;
+ }
+}
+
+static const struct zwp_primary_selection_device_v1_listener s_device_listener = {
+ .data_offer = device_data_offer,
+ .selection = device_selection,
+};
+
+/* ── pointer listener (serial capture) ─────────────────────────────────── */
+
+static void ptr_enter(void *d, struct wl_pointer *p, uint32_t serial,
+ struct wl_surface *surf, wl_fixed_t x, wl_fixed_t y)
+{ (void)d; (void)p; (void)serial; (void)surf; (void)x; (void)y; }
+
+static void ptr_leave(void *d, struct wl_pointer *p, uint32_t serial,
+ struct wl_surface *surf)
+{ (void)d; (void)p; (void)serial; (void)surf; }
+
+static void ptr_motion(void *d, struct wl_pointer *p, uint32_t t,
+ wl_fixed_t x, wl_fixed_t y)
+{ (void)d; (void)p; (void)t; (void)x; (void)y; }
+
+static void ptr_button(void *d, struct wl_pointer *p, uint32_t serial,
+ uint32_t t, uint32_t btn, uint32_t state)
+{ (void)d; (void)p; (void)t; (void)btn; (void)state; s_last_serial = serial; }
+
+static void ptr_axis(void *d, struct wl_pointer *p, uint32_t t,
+ uint32_t axis, wl_fixed_t val)
+{ (void)d; (void)p; (void)t; (void)axis; (void)val; }
+
+/* v5+ events — never sent to a version-1 proxy, but slots must be non-NULL
+ * only for the version we bind at. We bind at v1, so these are safe NULL. */
+static const struct wl_pointer_listener s_pointer_listener = {
+ .enter = ptr_enter,
+ .leave = ptr_leave,
+ .motion = ptr_motion,
+ .button = ptr_button,
+ .axis = ptr_axis,
+ .frame = NULL,
+ .axis_source = NULL,
+ .axis_stop = NULL,
+ .axis_discrete = NULL,
+ .axis_value120 = NULL,
+ .axis_relative_direction = NULL,
+};
+
+/* ── keyboard listener (serial capture) ────────────────────────────────── */
+
+static void kbd_keymap(void *d, struct wl_keyboard *kb, uint32_t fmt,
+ int32_t fd, uint32_t size)
+{ (void)d; (void)kb; (void)fmt; (void)size; close(fd); }
+
+static void kbd_enter(void *d, struct wl_keyboard *kb, uint32_t serial,
+ struct wl_surface *surf, struct wl_array *keys)
+{ (void)d; (void)kb; (void)serial; (void)surf; (void)keys; }
+
+static void kbd_leave(void *d, struct wl_keyboard *kb, uint32_t serial,
+ struct wl_surface *surf)
+{ (void)d; (void)kb; (void)serial; (void)surf; }
+
+static void kbd_key(void *d, struct wl_keyboard *kb, uint32_t serial,
+ uint32_t t, uint32_t key, uint32_t state)
+{ (void)d; (void)kb; (void)t; (void)key; (void)state; s_last_serial = serial; }
+
+static void kbd_modifiers(void *d, struct wl_keyboard *kb, uint32_t serial,
+ uint32_t dep, uint32_t lat, uint32_t lock, uint32_t grp)
+{ (void)d; (void)kb; (void)serial; (void)dep; (void)lat; (void)lock; (void)grp; }
+
+/* .repeat_info is v4 — never sent to a v1 proxy */
+static const struct wl_keyboard_listener s_keyboard_listener = {
+ .keymap = kbd_keymap,
+ .enter = kbd_enter,
+ .leave = kbd_leave,
+ .key = kbd_key,
+ .modifiers = kbd_modifiers,
+ .repeat_info = NULL,
+};
+
+/* ── seat listener ──────────────────────────────────────────────────────── */
+
+static void seat_capabilities(void *data, struct wl_seat *seat, uint32_t caps)
+{
+ (void)data;
+ if ((caps & WL_SEAT_CAPABILITY_POINTER) && !s_pointer) {
+ s_pointer = wl_seat_get_pointer(seat);
+ wl_pointer_add_listener(s_pointer, &s_pointer_listener, NULL);
+ }
+ if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !s_keyboard) {
+ s_keyboard = wl_seat_get_keyboard(seat);
+ wl_keyboard_add_listener(s_keyboard, &s_keyboard_listener, NULL);
+ }
+}
+
+/* .name is v2 — never sent to a v1 seat proxy */
+static const struct wl_seat_listener s_seat_listener = {
+ .capabilities = seat_capabilities,
+ .name = NULL,
+};
+
+/* ── registry listener ──────────────────────────────────────────────────── */
+
+static void registry_global(void *data, struct wl_registry *registry,
+ uint32_t name, const char *interface, uint32_t version)
+{
+ (void)data;
+ if (strcmp(interface, wl_seat_interface.name) == 0 && !s_seat) {
+ s_seat = wl_registry_bind(registry, name, &wl_seat_interface,
+ version < 1 ? version : 1);
+ wl_seat_add_listener(s_seat, &s_seat_listener, NULL);
+ } else if (strcmp(interface,
+ zwp_primary_selection_device_manager_v1_interface.name) == 0) {
+ s_manager = wl_registry_bind(registry, name,
+ &zwp_primary_selection_device_manager_v1_interface, 1);
+ }
+}
+
+static void registry_global_remove(void *data, struct wl_registry *registry,
+ uint32_t name)
+{ (void)data; (void)registry; (void)name; }
+
+static const struct wl_registry_listener s_registry_listener = {
+ .global = registry_global,
+ .global_remove = registry_global_remove,
+};
+
+/* ── public API ─────────────────────────────────────────────────────────── */
+
+void wayland_primary_init(SDL_Window *window)
+{
+ SDL_PropertiesID props = SDL_GetWindowProperties(window);
+ s_display = SDL_GetPointerProperty(props,
+ SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, NULL);
+ if (!s_display)
+ return;
+
+ s_registry = wl_display_get_registry(s_display);
+ wl_registry_add_listener(s_registry, &s_registry_listener, NULL);
+ /* Roundtrip: triggers registry_global and seat_capabilities callbacks. */
+ wl_display_roundtrip(s_display);
+
+ if (!s_manager || !s_seat)
+ return;
+
+ s_device = zwp_primary_selection_device_manager_v1_get_device(
+ s_manager, s_seat);
+ zwp_primary_selection_device_v1_add_listener(s_device,
+ &s_device_listener, NULL);
+ wl_display_flush(s_display);
+}
+
+void wayland_primary_set(const char *text)
+{
+ if (!s_device || !s_manager || !text || !*text)
+ return;
+ if (s_current_text && strcmp(s_current_text, text) == 0)
+ return;
+
+ if (s_source) {
+ zwp_primary_selection_source_v1_destroy(s_source);
+ s_source = NULL;
+ }
+ free(s_current_text);
+ s_current_text = strdup(text);
+
+ s_source = zwp_primary_selection_device_manager_v1_create_source(s_manager);
+ zwp_primary_selection_source_v1_add_listener(s_source,
+ &s_source_listener, NULL);
+ zwp_primary_selection_source_v1_offer(s_source, "text/plain;charset=utf-8");
+ zwp_primary_selection_source_v1_offer(s_source, "text/plain");
+ zwp_primary_selection_source_v1_offer(s_source, "UTF8_STRING");
+ zwp_primary_selection_source_v1_offer(s_source, "STRING");
+ zwp_primary_selection_source_v1_offer(s_source, "TEXT");
+
+ zwp_primary_selection_device_v1_set_selection(s_device, s_source,
+ s_last_serial);
+ wl_display_flush(s_display);
+}
+
+void wayland_primary_dispatch(void)
+{
+ if (!s_display)
+ return;
+ /*
+ * SDL's Wayland read thread reads events from the socket and places
+ * them in their respective queues. Our proxies use the default queue,
+ * so dispatch_pending processes whatever SDL's thread has already read.
+ * No socket read is needed here.
+ */
+ wl_display_dispatch_pending(s_display);
+}
diff --git a/wayland_primary.h b/wayland_primary.h
@@ -0,0 +1,17 @@
+#ifndef WAYLAND_PRIMARY_H
+#define WAYLAND_PRIMARY_H
+
+#include <SDL3/SDL_video.h>
+
+/* Initialize Wayland primary selection using SDL3's wl_display.
+ * Call once after the SDL window is created, before the event loop. */
+void wayland_primary_init(SDL_Window *window);
+
+/* Set the primary selection text. Skips if text is unchanged. */
+void wayland_primary_set(const char *text);
+
+/* Dispatch pending events on our Wayland proxies (default queue).
+ * Call once per main-loop iteration, after handle_events(). */
+void wayland_primary_dispatch(void);
+
+#endif