Repository: mpv-player/mpv
Pull request #16234: wayland: support clipboard writing
Author: login: na-na-hi
Created: 2025-04-12T22:34:36Z | Merged: 2025-04-22T22:07:48Z

This makes writing to clipboard property work on both the Wayland VO backend and `ext-data-control-v1` backend. 


Diff:
diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst
index ba558f288426c..4e594f5e3671b 100644
--- a/DOCS/man/input.rst
+++ b/DOCS/man/input.rst
@@ -4028,9 +4028,10 @@ Property list
 
     ``clipboard/text`` (RW)
         The text content in the clipboard (Windows, Wayland and macOS only).
-        Writing to this property sets the text clipboard content (Windows only).
+        Writing to this property sets the text clipboard content
+        (Windows, Wayland and macOS only).
 
-    ``clipboard/text-primary``
+    ``clipboard/text-primary`` (RW)
         The text content in the primary selection (Wayland only).
 
     .. note::
diff --git a/player/clipboard/clipboard-wayland.c b/player/clipboard/clipboard-wayland.c
index b3da3ff134574..9f53806fc863f 100644
--- a/player/clipboard/clipboard-wayland.c
+++ b/player/clipboard/clipboard-wayland.c
@@ -59,6 +59,8 @@ struct clipboard_wayland_seat {
     struct wl_list link;
     uint32_t id;
     struct ext_data_control_device_v1 *dcdev;
+    struct ext_data_control_source_v1 *selection_source;
+    struct ext_data_control_source_v1 *primary_selection_source;
     bool offered_plain_text;
 };
 
@@ -71,6 +73,10 @@ static void remove_seat(struct clipboard_wayland_seat *seat)
     wl_list_remove(&seat->link);
     if (seat->dcdev)
         ext_data_control_device_v1_destroy(seat->dcdev);
+    if (seat->selection_source)
+        ext_data_control_source_v1_destroy(seat->selection_source);
+    if (seat->primary_selection_source)
+        ext_data_control_source_v1_destroy(seat->primary_selection_source);
     wl_seat_destroy(seat->seat);
     talloc_free(seat);
     return;
@@ -172,6 +178,59 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/plain;charset=utf-8") == 0 ||
+        strcmp(mime_type, "text/plain") == 0 ||
+        strcmp(mime_type, "text") == 0)
+    {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
+        if (fdp.revents & (POLLERR | POLLHUP)) {
+            MP_VERBOSE(wl, "data source send aborted (write error)\n");
+            goto cleanup;
+        }
+
+        if (fdp.revents & POLLOUT) {
+            mp_mutex_lock(&wl->lock);
+            bstr text = data_source == s->selection_source ? wl->selection_text : wl->primary_selection_text;
+            ssize_t data_written = write(fd, text.start, text.len);
+            mp_mutex_unlock(&wl->lock);
+            if (data_written == -1) {
+                MP_VERBOSE(wl, "data source send aborted (write error: %s)\n", mp_strerror(errno));
+            } else {
+                MP_VERBOSE(wl, "%zu bytes written to the data source fd\n", data_written);
+            }
+        }
+    }
+
+cleanup:
+    close(fd);
+}
+
+static void data_source_handle_cancelled(void *data,
+                                         struct ext_data_control_source_v1 *data_source)
+{
+    // This can happen when another client sets selection, which invalidates the current source.
+    struct clipboard_wayland_seat *s = data;
+    ext_data_control_source_v1_destroy(data_source);
+    if (s->selection_source == data_source)
+        s->selection_source = NULL;
+    if (s->primary_selection_source == data_source)
+        s->primary_selection_source = NULL;
+}
+
+static const struct ext_data_control_source_v1_listener data_source_listener = {
+    data_source_handle_send,
+    data_source_handle_cancelled,
+};
+
 static void registry_handle_add(void *data, struct wl_registry *reg, uint32_t id,
                                 const char *interface, uint32_t ver)
 {
@@ -213,6 +272,35 @@ static const struct wl_registry_listener registry_listener = {
     registry_handle_remove,
 };
 
+static void seat_create_data_source(struct clipboard_wayland_seat *seat, bool is_primary)
+{
+    struct ext_data_control_source_v1 *source = ext_data_control_manager_v1_create_data_source(seat->wl->dcman);
+    ext_data_control_source_v1_offer(source, "text/plain;charset=utf-8");
+    ext_data_control_source_v1_offer(source, "text/plain");
+    ext_data_control_source_v1_offer(source, "text");
+    ext_data_control_source_v1_add_listener(source, &data_source_listener, seat);
+    if (is_primary) {
+        seat->primary_selection_source = source;
+        ext_data_control_device_v1_set_primary_selection(seat->dcdev, seat->primary_selection_source);
+    } else {
+        seat->selection_source = source;
+        ext_data_control_device_v1_set_selection(seat->dcdev, seat->selection_source);
+    }
+}
+
+static void create_data_sources(struct clipboard_wayland_priv *wl)
+{
+    struct clipboard_wayland_seat *seat;
+    wl_list_for_each(seat, &wl->seat_list, link) {
+        if (!seat->selection_source && wl->dcman) {
+            seat_create_data_source(seat, false);
+        }
+        if (!seat->primary_selection_source && wl->dcman) {
+            seat_create_data_source(seat, true);
+        }
+    }
+}
+
 static void clipboard_wayland_uninit(struct clipboard_wayland_priv *wl)
 {
     if (!wl)
@@ -294,7 +382,7 @@ static void get_selection_data(struct clipboard_wayland_priv *wl, struct clipboa
     }
 
     if (data_read == -1) {
-        MP_VERBOSE(wl, "data offer aborted (read error)\n");
+        MP_VERBOSE(wl, "data offer aborted (read error: %s)\n", mp_strerror(errno));
     } else {
         MP_VERBOSE(wl, "Read %zu bytes from the data offer fd\n", content.len);
         // Update clipboard text content
@@ -460,6 +548,30 @@ static int get_data(struct clipboard_ctx *cl, struct clipboard_access_params *pa
     return ret;
 }
 
+static int set_data(struct clipboard_ctx *cl, struct clipboard_access_params *params,
+                    struct clipboard_data *data)
+{
+    if (params->type != CLIPBOARD_DATA_TEXT || data->type != CLIPBOARD_DATA_TEXT)
+        return CLIPBOARD_FAILED;
+
+    struct clipboard_wayland_priv *priv = cl->priv;
+    bstr text = bstrdup(priv, bstr0(data->u.text));
+    mp_mutex_lock(&priv->lock);
+    switch (params->target) {
+    case CLIPBOARD_TARGET_CLIPBOARD:
+        talloc_free(priv->selection_text.start);
+        priv->selection_text = text;
+        break;
+    case CLIPBOARD_TARGET_PRIMARY_SELECTION:
+        talloc_free(priv->primary_selection_text.start);
+        priv->primary_selection_text = text;
+        break;
+    }
+    mp_mutex_unlock(&priv->lock);
+    create_data_sources(priv);
+    return CLIPBOARD_SUCCESS;
+}
+
 const struct clipboard_backend clipboard_backend_wayland = {
     .name = "wayland",
     .desc = "Wayland clipboard (Data control protocol)",
@@ -467,4 +579,5 @@ const struct clipboard_backend clipboard_backend_wayland = {
     .uninit = uninit,
     .data_changed = data_changed,
     .get_data = get_data,
+    .set_data = set_data,
 };
diff --git a/video/out/wayland_common.c b/video/out/wayland_common.c
index 2067d9f3c7115..d627717a9beab 100644
--- a/video/out/wayland_common.c
+++ b/video/out/wayland_common.c
@@ -192,14 +192,16 @@ struct vo_wayland_seat {
     struct wl_keyboard *keyboard;
     struct wl_pointer  *pointer;
     struct wl_touch    *touch;
-    struct wl_data_device *dnd_ddev;
+    struct wl_data_device *data_device;
     struct vo_wayland_data_offer *pending_offer;
     struct vo_wayland_data_offer *dnd_offer;
     struct vo_wayland_data_offer *selection_offer;
+    struct wl_data_source *data_source;
     struct vo_wayland_text_input *text_input;
     struct wp_cursor_shape_device_v1 *cursor_shape_device;
     uint32_t pointer_enter_serial;
     uint32_t pointer_button_serial;
+    uint32_t last_serial;
     struct xkb_keymap  *xkb_keymap;
     struct xkb_state   *xkb_state;
     uint32_t keyboard_code;
@@ -262,7 +264,7 @@ static void remove_feedback(struct vo_wayland_feedback_pool *fback_pool,
                             struct wp_presentation_feedback *fback);
 static void remove_output(struct vo_wayland_output *out);
 static void remove_seat(struct vo_wayland_seat *seat);
-static void seat_create_dnd_ddev(struct vo_wayland_seat *seat);
+static void seat_create_data_device(struct vo_wayland_seat *seat);
 static void seat_create_text_input(struct vo_wayland_seat *seat);
 static void request_decoration_mode(struct vo_wayland_state *wl, uint32_t mode);
 static void rescale_geometry(struct vo_wayland_state *wl, double old_scale);
@@ -280,6 +282,7 @@ static void pointer_handle_enter(void *data, struct wl_pointer *pointer,
 {
     struct vo_wayland_seat *s = data;
     struct vo_wayland_state *wl = s->wl;
+    s->last_serial = serial;
 
     s->pointer_enter_serial = serial;
     set_cursor_visibility(s, wl->cursor_visible);
@@ -298,6 +301,7 @@ static void pointer_handle_leave(void *data, struct wl_pointer *pointer,
 {
     struct vo_wayland_seat *s = data;
     struct vo_wayland_state *wl = s->wl;
+    s->last_serial = serial;
     mp_input_put_key(wl->vo->input_ctx, MP_KEY_MOUSE_LEAVE);
 }
 
@@ -321,6 +325,7 @@ static void pointer_handle_button(void *data, struct wl_pointer *wl_pointer,
 {
     struct vo_wayland_seat *s = data;
     struct vo_wayland_state *wl = s->wl;
+    s->last_serial = serial;
     state = state == WL_POINTER_BUTTON_STATE_PRESSED ? MP_KEY_STATE_DOWN
                                                      : MP_KEY_STATE_UP;
 
@@ -466,6 +471,7 @@ static void touch_handle_down(void *data, struct wl_touch *wl_touch,
 {
     struct vo_wayland_seat *s = data;
     struct vo_wayland_state *wl = s->wl;
+    s->last_serial = serial;
     // Note: the position should still be saved here for VO dragging handling.
     wl->mouse_x = handle_round(wl->scaling, wl_fixed_to_int(x_w));
     wl->mouse_y = handle_round(wl->scaling, wl_fixed_to_int(y_w));
@@ -490,6 +496,7 @@ static void touch_handle_up(void *data, struct wl_touch *wl_touch,
 {
     struct vo_wayland_seat *s = data;
     struct vo_wayland_state *wl = s->wl;
+    s->last_serial = serial;
     mp_input_remove_touch_point(wl->vo->input_ctx, id);
     wl->last_button_seat = NULL;
 }
@@ -587,6 +594,7 @@ static void keyboard_handle_enter(void *data, struct wl_keyboard *wl_keyboard,
 {
     struct vo_wayland_seat *s = data;
     struct vo_wayland_state *wl = s->wl;
+    s->last_serial = serial;
     s->has_keyboard_input = true;
     s->keyboard_entering = true;
     guess_focus(wl);
@@ -601,6 +609,7 @@ static void keyboard_handle_leave(void *data, struct wl_keyboard *wl_keyboard,
 {
     struct vo_wayland_seat *s = data;
     struct vo_wayland_state *wl = s->wl;
+    s->last_serial = serial;
     s->has_keyboard_input = false;
     s->keyboard_code = 0;
     s->mpkey = 0;
@@ -614,6 +623,7 @@ static void keyboard_handle_key(void *data, struct wl_keyboard *wl_keyboard,
                                 uint32_t state)
 {
     struct vo_wayland_seat *s = data;
+    s->last_serial = serial;
     handle_key_input(s, key, state, false);
 }
 
@@ -624,6 +634,7 @@ static void keyboard_handle_modifiers(void *data, struct wl_keyboard *wl_keyboar
 {
     struct vo_wayland_seat *s = data;
     struct vo_wayland_state *wl = s->wl;
+    s->last_serial = serial;
 
     if (s->xkb_state) {
         xkb_state_update_mask(s->xkb_state, mods_depressed, mods_latched,
@@ -888,6 +899,70 @@ static const struct wl_data_device_listener data_device_listener = {
     data_device_handle_selection,
 };
 
+static void data_source_handle_target(void *data, struct wl_data_source *wl_data_source,
+                                      const char *mime_type)
+{
+}
+
+static void data_source_handle_send(void *data, struct wl_data_source *wl_data_source,
+                                    const char *mime_type, int32_t fd)
+{
+    struct vo_wayland_seat *s = data;
+    struct vo_wayland_state *wl = s->wl;
+    int score = mp_event_get_mime_type_score(wl->vo->input_ctx, mime_type);
+    if (score >= 0) {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
+        if (fdp.revents & (POLLERR | POLLHUP)) {
+            MP_VERBOSE(wl, "data source send aborted (write error)\n");
+            goto cleanup;
+        }
+
+        if (fdp.revents & POLLOUT) {
+            ssize_t data_written = write(fd, wl->selection_text.start, wl->selection_text.len);
+            if (data_written == -1) {
+                MP_VERBOSE(wl, "data source send aborted (write error: %s)\n", mp_strerror(errno));
+            } else {
+                MP_VERBOSE(wl, "%zu bytes written to the data source fd\n", data_written);
+            }
+        }
+    }
+
+cleanup:
+    close(fd);
+}
+
+static void data_source_handle_cancelled(void *data, struct wl_data_source *wl_data_source)
+{
+    // This can happen when another client sets selection, which invalidates the current source.
+    struct vo_wayland_seat *s = data;
+    wl_data_source_destroy(wl_data_source);
+    s->data_source = NULL;
+}
+
+static void data_source_handle_dnd_drop_performed(void *data, struct wl_data_source *wl_data_source)
+{
+}
+
+static void data_source_handle_dnd_finished(void *data, struct wl_data_source *wl_data_source)
+{
+}
+
+static void data_source_handle_action(void *data, struct wl_data_source *wl_data_source,
+                                      uint32_t dnd_action)
+{
+}
+
+static const struct wl_data_source_listener data_source_listener = {
+    data_source_handle_target,
+    data_source_handle_send,
+    data_source_handle_cancelled,
+    data_source_handle_dnd_drop_performed,
+    data_source_handle_dnd_finished,
+    data_source_handle_action,
+};
+
 static void enable_ime(struct vo_wayland_text_input *ti)
 {
     if (!ti->has_focus)
@@ -1963,7 +2038,7 @@ static void registry_handle_add(void *data, struct wl_registry *reg, uint32_t id
         wl_list_insert(&wl->seat_list, &seat->link);
 
         if (wl->devman)
-            seat_create_dnd_ddev(seat);
+            seat_create_data_device(seat);
 
         if (wl->text_input_manager)
             seat_create_text_input(seat);
@@ -2152,7 +2227,7 @@ static void check_fd(struct vo_wayland_state *wl, struct vo_wayland_data_offer *
         }
 
         if (data_read == -1) {
-            MP_VERBOSE(wl, "data offer aborted (read error)\n");
+            MP_VERBOSE(wl, "data offer aborted (read error: %s)\n", mp_strerror(errno));
         } else {
             MP_VERBOSE(wl, "Read %zu bytes from the data offer fd\n", content.len);
 
@@ -2561,8 +2636,8 @@ static void remove_seat(struct vo_wayland_seat *seat)
         wl_pointer_destroy(seat->pointer);
     if (seat->touch)
         wl_touch_destroy(seat->touch);
-    if (seat->dnd_ddev)
-        wl_data_device_destroy(seat->dnd_ddev);
+    if (seat->data_device)
+        wl_data_device_destroy(seat->data_device);
     if (seat->text_input)
         zwp_text_input_v3_destroy(seat->text_input->text_input);
 #if HAVE_WAYLAND_PROTOCOLS_1_32
@@ -2578,15 +2653,28 @@ static void remove_seat(struct vo_wayland_seat *seat)
     destroy_offer(seat->dnd_offer);
     destroy_offer(seat->selection_offer);
 
+    if (seat->data_source)
+        wl_data_source_destroy(seat->data_source);
+
     wl_seat_destroy(seat->seat);
     talloc_free(seat);
     return;
 }
 
-static void seat_create_dnd_ddev(struct vo_wayland_seat *seat)
+static void seat_create_data_source(struct vo_wayland_seat *seat)
 {
-    seat->dnd_ddev = wl_data_device_manager_get_data_device(seat->wl->devman, seat->seat);
-    wl_data_device_add_listener(seat->dnd_ddev, &data_device_listener, seat);
+    seat->data_source = wl_data_device_manager_create_data_source(seat->wl->devman);
+    wl_data_source_offer(seat->data_source, "text/plain;charset=utf-8");
+    wl_data_source_offer(seat->data_source, "text/plain");
+    wl_data_source_offer(seat->data_source, "text");
+    wl_data_source_add_listener(seat->data_source, &data_source_listener, seat);
+    wl_data_device_set_selection(seat->data_device, seat->data_source, seat->last_serial);
+}
+
+static void seat_create_data_device(struct vo_wayland_seat *seat)
+{
+    seat->data_device = wl_data_device_manager_get_data_device(seat->wl->devman, seat->seat);
+    wl_data_device_add_listener(seat->data_device, &data_device_listener, seat);
 }
 
 static void seat_create_text_input(struct vo_wayland_seat *seat)
@@ -3206,6 +3294,22 @@ int vo_wayland_control(struct vo *vo, int *events, int request, void *arg)
         vc->data.u.text = bstrto0(vc->talloc_ctx, wl->selection_text);
         return VO_TRUE;
     }
+    case VOCTRL_SET_CLIPBOARD: {
+        struct voctrl_clipboard *vc = arg;
+        // TODO: add primary selection support
+        if (vc->params.target != CLIPBOARD_TARGET_CLIPBOARD || vc->params.type != CLIPBOARD_DATA_TEXT)
+            return VO_NOTIMPL;
+        if (vc->data.type != CLIPBOARD_DATA_TEXT)
+            return VO_NOTIMPL;
+        talloc_free(wl->selection_text.start);
+        wl->selection_text = bstrdup(wl, bstr0(vc->data.u.text));
+        struct vo_wayland_seat *seat;
+        wl_list_for_each(seat, &wl->seat_list, link) {
+            if (seat->last_serial && !seat->data_source && wl->devman)
+                seat_create_data_source(seat);
+        }
+        return VO_TRUE;
+    }
     }
 
     return VO_NOTIMPL;
@@ -3360,8 +3464,8 @@ bool vo_wayland_init(struct vo *vo)
     if (wl->devman) {
         struct vo_wayland_seat *seat;
         wl_list_for_each(seat, &wl->seat_list, link) {
-            if (!seat->dnd_ddev)
-                seat_create_dnd_ddev(seat);
+            if (!seat->data_device)
+                seat_create_data_device(seat);
         }
     } else {
         MP_VERBOSE(wl, "Compositor doesn't support the %s (ver. 3) protocol!\n",


Reviews:
user: login: kasper93
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:55:59Z
user: login: kasper93
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:56:35Z
user: login: na-na-hi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T02:44:08Z
user: login: na-na-hi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T02:45:44Z
user: login: kasper93
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T08:59:27Z
user: login: kasper93
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:03:36Z
user: login: Dudemanguy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T23:19:31Z
user: login: Dudemanguy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T23:24:29Z
user: login: na-na-hi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T05:55:03Z
user: login: na-na-hi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T05:57:32Z
user: login: na-na-hi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T06:11:31Z
user: login: na-na-hi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T06:11:36Z
user: login: Dudemanguy
body: Works on my end.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-21T15:35:57Z

Review comments:
diff_hunk: @@ -172,6 +178,60 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/uri-list") == 0 ||
path: player/clipboard/clipboard-wayland.c
user: login: kasper93
body: `text/uri-list` is used only here, and not offered below.
created_at: 2025-04-14T15:55:59Z
author_association: MEMBER
diff_hunk: @@ -3206,6 +3294,22 @@ int vo_wayland_control(struct vo *vo, int *events, int request, void *arg)
         vc->data.u.text = bstrto0(vc->talloc_ctx, wl->selection_text);
         return VO_TRUE;
     }
+    case VOCTRL_SET_CLIPBOARD: {
+        struct voctrl_clipboard *vc = arg;
+        // TODO: add primary selection support
+        if (vc->params.target != CLIPBOARD_TARGET_CLIPBOARD || vc->params.type != CLIPBOARD_DATA_TEXT)
+            return VO_NOTIMPL;
+        if (vc->data.type != CLIPBOARD_DATA_TEXT)
+            return VO_NOTIMPL;
+        talloc_free(wl->selection_text.start);
+        wl->selection_text = bstrdup(wl, bstr0(vc->data.u.text));
path: video/out/wayland_common.c
user: login: kasper93
body: ```
bstrdup0
```
created_at: 2025-04-14T15:56:35Z
author_association: MEMBER
diff_hunk: @@ -172,6 +178,60 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/uri-list") == 0 ||
path: player/clipboard/clipboard-wayland.c
user: login: na-na-hi
body: Removed `text/uri-list` here.
created_at: 2025-04-15T02:44:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3206,6 +3294,22 @@ int vo_wayland_control(struct vo *vo, int *events, int request, void *arg)
         vc->data.u.text = bstrto0(vc->talloc_ctx, wl->selection_text);
         return VO_TRUE;
     }
+    case VOCTRL_SET_CLIPBOARD: {
+        struct voctrl_clipboard *vc = arg;
+        // TODO: add primary selection support
+        if (vc->params.target != CLIPBOARD_TARGET_CLIPBOARD || vc->params.type != CLIPBOARD_DATA_TEXT)
+            return VO_NOTIMPL;
+        if (vc->data.type != CLIPBOARD_DATA_TEXT)
+            return VO_NOTIMPL;
+        talloc_free(wl->selection_text.start);
+        wl->selection_text = bstrdup(wl, bstr0(vc->data.u.text));
path: video/out/wayland_common.c
user: login: na-na-hi
body: `bstrdup0` duplicates a bstr to a C string. Here the code duplicates a C string to a bstr and there is no existing function for this. 
created_at: 2025-04-15T02:45:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3206,6 +3294,22 @@ int vo_wayland_control(struct vo *vo, int *events, int request, void *arg)
         vc->data.u.text = bstrto0(vc->talloc_ctx, wl->selection_text);
         return VO_TRUE;
     }
+    case VOCTRL_SET_CLIPBOARD: {
+        struct voctrl_clipboard *vc = arg;
+        // TODO: add primary selection support
+        if (vc->params.target != CLIPBOARD_TARGET_CLIPBOARD || vc->params.type != CLIPBOARD_DATA_TEXT)
+            return VO_NOTIMPL;
+        if (vc->data.type != CLIPBOARD_DATA_TEXT)
+            return VO_NOTIMPL;
+        talloc_free(wl->selection_text.start);
+        wl->selection_text = bstrdup(wl, bstr0(vc->data.u.text));
path: video/out/wayland_common.c
user: login: kasper93
body: interesting
created_at: 2025-04-15T08:59:27Z
author_association: MEMBER
diff_hunk: @@ -172,6 +178,59 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/plain;charset=utf-8") == 0 ||
+        strcmp(mime_type, "text/plain") == 0 ||
+        strcmp(mime_type, "text") == 0)
+    {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
+        if (fdp.revents & (POLLERR | POLLHUP)) {
+            MP_VERBOSE(wl, "data source send aborted (write error)\n");
+            goto cleanup;
+        }
+
+        if (fdp.revents & POLLOUT) {
+            mp_mutex_lock(&wl->lock);
+            bstr text = data_source == s->selection_source ? wl->selection_text : wl->primary_selection_text;
+            ssize_t data_written = write(fd, text.start, text.len);
+            mp_mutex_unlock(&wl->lock);
+            if (data_written == -1) {
+                MP_VERBOSE(wl, "data source send aborted (write error)\n");
+            } else {
+                MP_VERBOSE(wl, "Wrote %zu bytes for the data source send fd\n", data_written);
path: player/clipboard/clipboard-wayland.c
user: login: kasper93
body: nit, but this triggers my ocd. Capitalize sentences the same way pls.
created_at: 2025-04-16T22:03:35Z
author_association: MEMBER
diff_hunk: @@ -172,6 +178,59 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/plain;charset=utf-8") == 0 ||
+        strcmp(mime_type, "text/plain") == 0 ||
+        strcmp(mime_type, "text") == 0)
+    {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
+        if (fdp.revents & (POLLERR | POLLHUP)) {
+            MP_VERBOSE(wl, "data source send aborted (write error)\n");
+            goto cleanup;
+        }
+
+        if (fdp.revents & POLLOUT) {
+            mp_mutex_lock(&wl->lock);
+            bstr text = data_source == s->selection_source ? wl->selection_text : wl->primary_selection_text;
+            ssize_t data_written = write(fd, text.start, text.len);
+            mp_mutex_unlock(&wl->lock);
+            if (data_written == -1) {
+                MP_VERBOSE(wl, "data source send aborted (write error)\n");
+            } else {
+                MP_VERBOSE(wl, "Wrote %zu bytes for the data source send fd\n", data_written);
path: player/clipboard/clipboard-wayland.c
user: login: Dudemanguy
body: Wording wise, I would prefer something like `%zu bytes written to the data source fd` here.
created_at: 2025-04-16T23:19:31Z
author_association: MEMBER
diff_hunk: @@ -172,6 +178,59 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/plain;charset=utf-8") == 0 ||
+        strcmp(mime_type, "text/plain") == 0 ||
+        strcmp(mime_type, "text") == 0)
+    {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
path: player/clipboard/clipboard-wayland.c
user: login: Dudemanguy
body: We don't need this right? If there's an error, `POLLERR` and `POLLHUP` should catch it.
created_at: 2025-04-16T23:22:49Z
author_association: MEMBER
diff_hunk: @@ -172,6 +178,59 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/plain;charset=utf-8") == 0 ||
+        strcmp(mime_type, "text/plain") == 0 ||
+        strcmp(mime_type, "text") == 0)
+    {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
+        if (fdp.revents & (POLLERR | POLLHUP)) {
+            MP_VERBOSE(wl, "data source send aborted (write error)\n");
path: player/clipboard/clipboard-wayland.c
user: login: Dudemanguy
body: Could use errno here in the log.
created_at: 2025-04-16T23:23:17Z
author_association: MEMBER
diff_hunk: @@ -172,6 +178,59 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/plain;charset=utf-8") == 0 ||
+        strcmp(mime_type, "text/plain") == 0 ||
+        strcmp(mime_type, "text") == 0)
+    {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
+        if (fdp.revents & (POLLERR | POLLHUP)) {
+            MP_VERBOSE(wl, "data source send aborted (write error)\n");
+            goto cleanup;
+        }
+
+        if (fdp.revents & POLLOUT) {
+            mp_mutex_lock(&wl->lock);
+            bstr text = data_source == s->selection_source ? wl->selection_text : wl->primary_selection_text;
+            ssize_t data_written = write(fd, text.start, text.len);
+            mp_mutex_unlock(&wl->lock);
+            if (data_written == -1) {
+                MP_VERBOSE(wl, "data source send aborted (write error)\n");
path: player/clipboard/clipboard-wayland.c
user: login: Dudemanguy
body: Same here.
created_at: 2025-04-16T23:23:45Z
author_association: MEMBER
diff_hunk: @@ -172,6 +178,59 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/plain;charset=utf-8") == 0 ||
+        strcmp(mime_type, "text/plain") == 0 ||
+        strcmp(mime_type, "text") == 0)
+    {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
+        if (fdp.revents & (POLLERR | POLLHUP)) {
+            MP_VERBOSE(wl, "data source send aborted (write error)\n");
+            goto cleanup;
+        }
+
+        if (fdp.revents & POLLOUT) {
+            mp_mutex_lock(&wl->lock);
+            bstr text = data_source == s->selection_source ? wl->selection_text : wl->primary_selection_text;
+            ssize_t data_written = write(fd, text.start, text.len);
+            mp_mutex_unlock(&wl->lock);
+            if (data_written == -1) {
+                MP_VERBOSE(wl, "data source send aborted (write error)\n");
+            } else {
+                MP_VERBOSE(wl, "Wrote %zu bytes for the data source send fd\n", data_written);
path: player/clipboard/clipboard-wayland.c
user: login: na-na-hi
body: Changed the wording. 
created_at: 2025-04-17T05:55:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,6 +178,59 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/plain;charset=utf-8") == 0 ||
+        strcmp(mime_type, "text/plain") == 0 ||
+        strcmp(mime_type, "text") == 0)
+    {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
path: player/clipboard/clipboard-wayland.c
user: login: na-na-hi
body: `POLLERR` and `POLLHUP` return means that `poll()` has already succeeded and won't return 0 or -1, so the condition here is separate from the `revents` errors. 
created_at: 2025-04-17T05:57:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,6 +178,59 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/plain;charset=utf-8") == 0 ||
+        strcmp(mime_type, "text/plain") == 0 ||
+        strcmp(mime_type, "text") == 0)
+    {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
+        if (fdp.revents & (POLLERR | POLLHUP)) {
+            MP_VERBOSE(wl, "data source send aborted (write error)\n");
+            goto cleanup;
+        }
+
+        if (fdp.revents & POLLOUT) {
+            mp_mutex_lock(&wl->lock);
+            bstr text = data_source == s->selection_source ? wl->selection_text : wl->primary_selection_text;
+            ssize_t data_written = write(fd, text.start, text.len);
+            mp_mutex_unlock(&wl->lock);
+            if (data_written == -1) {
+                MP_VERBOSE(wl, "data source send aborted (write error)\n");
path: player/clipboard/clipboard-wayland.c
user: login: na-na-hi
body: Added errno messages, and also for other Wayland read/write error messages. 
created_at: 2025-04-17T06:11:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,6 +178,59 @@ static const struct ext_data_control_device_v1_listener data_device_listener = {
     data_device_handle_primary_selection,
 };
 
+static void data_source_handle_send(void *data,
+                                    struct ext_data_control_source_v1 *data_source,
+                                    const char *mime_type,
+                                    int32_t fd)
+{
+    struct clipboard_wayland_seat *s = data;
+    struct clipboard_wayland_priv *wl = s->wl;
+    if (strcmp(mime_type, "text/plain;charset=utf-8") == 0 ||
+        strcmp(mime_type, "text/plain") == 0 ||
+        strcmp(mime_type, "text") == 0)
+    {
+        struct pollfd fdp = { .fd = fd, .events = POLLOUT };
+        if (poll(&fdp, 1, 0) <= 0)
+            goto cleanup;
+        if (fdp.revents & (POLLERR | POLLHUP)) {
+            MP_VERBOSE(wl, "data source send aborted (write error)\n");
path: player/clipboard/clipboard-wayland.c
user: login: na-na-hi
body: errno isn't set for `POLLERR` and `POLLHUP`. It's only set for EINTR and ENOMEM, both rarely happen and I don't think describing them is necessary. 
created_at: 2025-04-17T06:11:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-12T22:50:42Z
body: Download the artifacts for this pull request:
<details><summary>Windows</summary>

* [mpv-aarch64-pc-windows-msvc](https://nightly.link/mpv-player/mpv/actions/artifacts/2961531299.zip)
* [mpv-i686-w64-mingw32](https://nightly.link/mpv-player/mpv/actions/artifacts/2961484324.zip)
* [mpv-x86_64-pc-windows-msvc](https://nightly.link/mpv-player/mpv/actions/artifacts/2961526537.zip)
* [mpv-x86_64-w64-mingw32](https://nightly.link/mpv-player/mpv/actions/artifacts/2961490566.zip)
</details>
<details><summary>macOS</summary>

* [mpv-macos-13-intel](https://nightly.link/mpv-player/mpv/actions/artifacts/2961479382.zip)
* [mpv-macos-14-arm](https://nightly.link/mpv-player/mpv/actions/artifacts/2961509535.zip)
* [mpv-macos-15-arm](https://nightly.link/mpv-player/mpv/actions/artifacts/2961472073.zip)
</details>
author_association: NONE
user: login: guidocella
created_at: 2025-04-13T09:34:35Z
body: In the wayland backend writing seems to conflict with `wl-clip-persist`. With it running the clipboard is only updated after copying something else outside of mpv or opening the clipboard manager 2 times.
author_association: CONTRIBUTOR
user: login: llyyr
created_at: 2025-04-13T12:10:17Z
body: Could you move `clipboard-wayland: read already sent data when the fd is hung up` to it's own PR? This is purely a bug fix and shouldn't be held here until someone reviews the rest of the PR. That commit specifically lgtm
author_association: CONTRIBUTOR
user: login: na-na-hi
created_at: 2025-04-13T12:47:18Z
body: > In the wayland backend writing seems to conflict with `wl-clip-persist`. With it running the clipboard is only updated after copying something else outside of mpv or opening the clipboard manager 2 times.

Right now other clients won't know when mpv updates the clipboard content. This requires changing the way writing is handled, will do that later. 

> Could you move `clipboard-wayland: read already sent data when the fd is hung up` to it's own PR? This is purely a bug fix and shouldn't be held here until someone reviews the rest of the PR. That commit specifically lgtm

Done.

author_association: CONTRIBUTOR
user: login: llyyr
created_at: 2025-04-21T15:37:44Z
body: Could you squash `wayland: print errno strerror for data offer read/write` into the original commits? Not sure why it needs to be a different commit
author_association: CONTRIBUTOR
user: login: Dudemanguy
created_at: 2025-04-22T01:36:54Z
body: It's fine imo. It technically modifies another part.
author_association: MEMBER