diff options
| author | Berke Güzel <wenekar1@gmail.com> | 2026-10-13 13:44:13 +0300 |
|---|---|---|
| committer | Berke Güzel <wenekar1@gmail.com> | 2026-10-13 13:44:13 +0300 |
| commit | c059e3aa7f93da470f052a5cd8ee0d8dc969e679 (patch) | |
| tree | 63ffa378287ed2c606eda2e940501b9edc3d2b8b /src/platform/wayland/clipboard.c | |
| parent | 87d0bc50d53dc216efb2b04495f090616318bc5c (diff) | |
rename and simplification
Diffstat (limited to 'src/platform/wayland/clipboard.c')
| -rw-r--r-- | src/platform/wayland/clipboard.c | 629 |
1 files changed, 629 insertions, 0 deletions
diff --git a/src/platform/wayland/clipboard.c b/src/platform/wayland/clipboard.c new file mode 100644 index 0000000..a6922c4 --- /dev/null +++ b/src/platform/wayland/clipboard.c @@ -0,0 +1,629 @@ +#define _GNU_SOURCE +#include "internal.h" +#include "clipboard.h" + +#define CLIPBOARD_TEXT_MAX (256u * 1024) + +struct offer { + struct offer *next; + struct wl_data_offer *proxy; + char *mime; + bool uri_list; + bool tab; +}; +struct primary_offer { + struct primary_offer *next; + struct zwp_primary_selection_offer_v1 *proxy; + char *mime; +}; +struct transfer { + struct transfer *next; + struct clipboard *owner; + struct offer *drop; + struct wl_surface *drop_surface; + int fd, id; + char *text; + size_t len, offset, capacity; + bool sending; + bool uri_list; +}; +struct clipboard { + struct wl_display *display; + struct wl_data_device_manager *manager; + struct wl_data_device *device; + struct zwp_primary_selection_device_manager_v1 *primary_manager; + struct zwp_primary_selection_device_v1 *primary_device; + struct zwp_primary_selection_source_v1 *primary_source; + struct wl_seat *seat; + struct wl_data_source *source; + struct offer *selection; + struct offer *pending_offers; + struct offer *drag_offer; + struct primary_offer *primary_selection; + struct primary_offer *primary_pending_offers; + struct yasl_loop *loop; + struct transfer *transfers; + char *text; + char *primary_text; + clipboard_receive_fn receive; + void *ud; + clipboard_drag_fn drag; + void *drag_ud; + clipboard_drop_fn drop; + void *drop_ud; + wl_fixed_t drag_x, drag_y; + struct wl_surface *drag_surface; +}; + +const char *clipboard_drag_mime(void) +{ + static char mime[64]; + if (!mime[0]) + snprintf(mime, sizeof(mime), "application/x-yasl-tab;pid=%ld", (long)getpid()); + return mime; +} + +static void drag_event(struct clipboard *c, enum yasl_drag_event ev, + wl_fixed_t x, wl_fixed_t y) +{ + if (c->drag) + c->drag(ev, c->drag_surface, wl_fixed_to_double(x), wl_fixed_to_double(y), + c->drag_ud); +} + +static bool offer_droppable(const struct offer *o) +{ + return o && !o->tab && (o->uri_list || o->mime); +} + +static void drop_event(struct clipboard *c, enum yasl_drop_event ev) +{ + if (c->drop && c->drag_surface) + c->drop(ev, c->drag_surface, wl_fixed_to_double(c->drag_x), + wl_fixed_to_double(c->drag_y), c->drop_ud); +} + +static void free_offer(struct offer *o); + +static void transfer_end(struct transfer *t, bool complete) +{ + struct transfer **p = &t->owner->transfers; + while (*p && *p != t) p = &(*p)->next; + if (*p) *p = t->next; + yasl_loop_del_fd(t->owner->loop, t->id); + close(t->fd); + if (t->drop) { + if (complete && wl_data_offer_get_version(t->drop->proxy) >= + WL_DATA_OFFER_FINISH_SINCE_VERSION) + wl_data_offer_finish(t->drop->proxy); + free_offer(t->drop); + wl_display_flush(t->owner->display); + } + free(t->text); + free(t); +} + +static void transfer_destroy(struct transfer *t) +{ + transfer_end(t, false); +} + +static int transfer_io(int fd, short events, void *ud) +{ + struct transfer *t = ud; + if (events & (POLLERR | POLLNVAL)) { transfer_destroy(t); return 0; } + if (t->sending) { + ssize_t n = write(fd, t->text + t->offset, t->len - t->offset); + if (n > 0) t->offset += (size_t)n; + if (t->offset == t->len || n == 0 || + (n < 0 && errno != EAGAIN && errno != EINTR)) transfer_destroy(t); + } else { + if (t->capacity - t->len < 4096) { + size_t capacity = t->capacity ? t->capacity * 2 : 8192; + if (capacity > CLIPBOARD_TEXT_MAX) capacity = CLIPBOARD_TEXT_MAX; + if (capacity <= t->len) { transfer_destroy(t); return 0; } + char *text = realloc(t->text, capacity); + if (!text) { transfer_destroy(t); return 0; } + t->text = text; + t->capacity = capacity; + } + ssize_t n = read(fd, t->text + t->len, t->capacity - t->len); + if (n > 0) { + t->len += (size_t)n; + } else if (n == 0) { + bool valid = yasl_utf8_validate(t->text, t->len); + if (valid) + t->owner->receive(t->text ? t->text : "", t->len, t->uri_list, + t->drop_surface, t->owner->ud); + transfer_end(t, valid); + } else if (errno != EAGAIN && errno != EINTR) transfer_destroy(t); + } + return 0; +} + +static struct transfer *start_transfer_text(struct clipboard *c, int fd, bool sending, + bool uri_list, const char *send_text) +{ + struct transfer *t = calloc(1, sizeof(*t)); + if (!t) { close(fd); return NULL; } + t->owner = c; t->fd = fd; t->sending = sending; t->uri_list = uri_list; + fcntl(fd, F_SETFD, FD_CLOEXEC); + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); + if (sending) { + t->text = strdup(send_text ? send_text : ""); + if (!t->text) { close(fd); free(t); return NULL; } + t->len = strlen(t->text); + } + t->id = yasl_loop_add_fd(c->loop, fd, sending ? POLLOUT : POLLIN, transfer_io, t); + if (t->id < 0) { close(fd); free(t->text); free(t); return NULL; } + t->next = c->transfers; c->transfers = t; + return t; +} + +static struct transfer *start_transfer(struct clipboard *c, int fd, bool sending, bool uri_list) +{ + return start_transfer_text(c, fd, sending, uri_list, c->text); +} + +static const char *match_text_mime(const char *mime) +{ + if (unlikely(!mime)) return NULL; + if (likely(!strncasecmp(mime, "text/plain", 10))) { + if (!strcasecmp(mime, "text/plain;charset=utf-8")) return "text/plain;charset=utf-8"; + if (!strcasecmp(mime, "text/plain;charset=utf8")) return "text/plain;charset=utf8"; + if (!strcasecmp(mime, "text/plain")) return "text/plain"; + } + if (!strcasecmp(mime, "UTF8_STRING")) return "UTF8_STRING"; + if (!strcasecmp(mime, "STRING")) return "STRING"; + if (!strcasecmp(mime, "TEXT")) return "TEXT"; + return NULL; +} + +static void offer_keep_mime(char **slot, const char *mime) +{ + char *copy = strdup(mime); + if (!copy) return; + free(*slot); + *slot = copy; +} + +static void offer_mime(void *ud, struct wl_data_offer *proxy, const char *mime) +{ + (void)proxy; + struct offer *o = ud; + if (!strcmp(mime, clipboard_drag_mime())) o->tab = true; + if (!strcmp(mime, "text/uri-list")) o->uri_list = true; + const char *match = match_text_mime(mime); + if (match) { + if (!o->mime || !strcasecmp(match, "text/plain;charset=utf-8")) + offer_keep_mime(&o->mime, mime); + } +} +static void offer_source_actions(void *ud, struct wl_data_offer *proxy, uint32_t actions) +{ (void)ud; (void)proxy; (void)actions; } +static void offer_action(void *ud, struct wl_data_offer *proxy, uint32_t action) +{ (void)ud; (void)proxy; (void)action; } +static const struct wl_data_offer_listener offer_listener = { + .offer = offer_mime, + .source_actions = offer_source_actions, + .action = offer_action, +}; +static void free_offer(struct offer *o) +{ + if (o) { wl_data_offer_destroy(o->proxy); free(o->mime); free(o); } +} +static void take_pending_offer(struct clipboard *c, struct offer *o) +{ + if (!o) return; + struct offer **p = &c->pending_offers; + while (*p && *p != o) p = &(*p)->next; + if (*p) *p = o->next; + o->next = NULL; +} +static void clear_pending_offers(struct clipboard *c) +{ + while (c->pending_offers) { + struct offer *o = c->pending_offers; + c->pending_offers = o->next; + free_offer(o); + } +} +static void data_offer(void *ud, struct wl_data_device *device, struct wl_data_offer *proxy) +{ + (void)device; + struct clipboard *c = ud; + struct offer *o = calloc(1, sizeof(*o)); + if (!o) { wl_data_offer_destroy(proxy); return; } + o->proxy = proxy; + wl_data_offer_add_listener(proxy, &offer_listener, o); + o->next = c->pending_offers; + c->pending_offers = o; +} +static void selection(void *ud, struct wl_data_device *device, struct wl_data_offer *proxy) +{ + (void)device; + struct clipboard *c = ud; + struct offer *o = proxy ? wl_data_offer_get_user_data(proxy) : NULL; + take_pending_offer(c, o); + if (c->drag_offer == o) c->drag_offer = NULL; + if (c->selection != o) free_offer(c->selection); + c->selection = o; +} +static void drag_offer_enter(void *ud, struct wl_data_device *device, uint32_t serial, + struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, struct wl_data_offer *proxy) +{ + (void)device; + if (proxy) { + struct clipboard *c = ud; + struct offer *o = wl_data_offer_get_user_data(proxy); + take_pending_offer(c, o); + if (c->selection == o) c->selection = NULL; + if (c->drag_offer && c->drag_offer != o) free_offer(c->drag_offer); + c->drag_offer = o; + const char *mime = o && o->tab ? clipboard_drag_mime() + : o && o->uri_list ? "text/uri-list" + : o ? o->mime : NULL; + wl_data_offer_accept(proxy, serial, mime); + if (mime && wl_data_offer_get_version(proxy) >= 3) { + uint32_t action = o->tab ? WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE + : WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY; + wl_data_offer_set_actions(proxy, action, action); + } + c->drag_x = x; + c->drag_y = y; + c->drag_surface = surface; + if (o && o->tab) + drag_event(c, YASL_DRAG_OVER, x, y); + else if (offer_droppable(o)) + drop_event(c, YASL_DROP_OVER); + } +} +static void drag_offer_clear(void *ud, struct wl_data_device *device) +{ + (void)device; + struct clipboard *c = ud; + if (c->drag_offer && c->drag_offer->tab) + drag_event(c, YASL_DRAG_LEAVE, c->drag_x, c->drag_y); + else if (offer_droppable(c->drag_offer)) + drop_event(c, YASL_DROP_LEAVE); + free_offer(c->drag_offer); + c->drag_offer = NULL; + c->drag_surface = NULL; +} +static void drag_motion(void *ud, struct wl_data_device *device, uint32_t time, wl_fixed_t x, wl_fixed_t y) +{ + (void)device; (void)time; + struct clipboard *c = ud; + c->drag_x = x; + c->drag_y = y; + if (c->drag_offer && c->drag_offer->tab) + drag_event(c, YASL_DRAG_OVER, x, y); + else if (offer_droppable(c->drag_offer)) + drop_event(c, YASL_DROP_OVER); +} +static void drag_drop(void *ud, struct wl_data_device *device) +{ + (void)device; + struct clipboard *c = ud; + struct offer *o = c->drag_offer; + if (!o) return; + c->drag_offer = NULL; + bool v3 = wl_data_offer_get_version(o->proxy) >= 3; + if (o->tab) { + drag_event(c, YASL_DRAG_DROP, c->drag_x, c->drag_y); + if (v3) + wl_data_offer_finish(o->proxy); + } else if (offer_droppable(o)) { + drop_event(c, YASL_DROP_LEAVE); + int fds[2]; + if (pipe2(fds, O_CLOEXEC) == 0) { + wl_data_offer_receive(o->proxy, o->uri_list ? "text/uri-list" : o->mime, fds[1]); + close(fds[1]); + struct transfer *t = start_transfer(c, fds[0], false, o->uri_list); + if (t) { + t->drop = o; + t->drop_surface = c->drag_surface; + o = NULL; + } + wl_display_flush(c->display); + } + } + c->drag_surface = NULL; + free_offer(o); +} +static const struct wl_data_device_listener device_listener = { + .data_offer = data_offer, .selection = selection, .enter = drag_offer_enter, + .leave = drag_offer_clear, .motion = drag_motion, .drop = drag_drop +}; +static void source_target(void *ud, struct wl_data_source *s, const char *mime) { (void)ud; (void)s; (void)mime; } +static void source_send(void *ud, struct wl_data_source *s, const char *mime, int32_t fd) +{ (void)s; (void)mime; start_transfer(ud, fd, true, false); } +static void source_cancelled(void *ud, struct wl_data_source *s) +{ + struct clipboard *c = ud; + if (c->source == s) c->source = NULL; + wl_data_source_destroy(s); +} +static void source_dnd_drop_performed(void *ud, struct wl_data_source *s) { (void)ud; (void)s; } +static void source_dnd_finished(void *ud, struct wl_data_source *s) { (void)ud; (void)s; } +static void source_action(void *ud, struct wl_data_source *s, uint32_t action) { (void)ud; (void)s; (void)action; } +static const struct wl_data_source_listener source_listener = { + .target = source_target, .send = source_send, .cancelled = source_cancelled, + .dnd_drop_performed = source_dnd_drop_performed, + .dnd_finished = source_dnd_finished, .action = source_action, +}; + +static void free_primary_offer(struct primary_offer *o) +{ + if (o) { zwp_primary_selection_offer_v1_destroy(o->proxy); free(o->mime); free(o); } +} + +static void take_pending_primary_offer(struct clipboard *c, struct primary_offer *o) +{ + if (!o) return; + struct primary_offer **p = &c->primary_pending_offers; + while (*p && *p != o) p = &(*p)->next; + if (*p) *p = o->next; + o->next = NULL; +} + +static void clear_pending_primary_offers(struct clipboard *c) +{ + while (c->primary_pending_offers) { + struct primary_offer *o = c->primary_pending_offers; + c->primary_pending_offers = o->next; + free_primary_offer(o); + } +} + +static void primary_offer_mime(void *ud, struct zwp_primary_selection_offer_v1 *proxy, const char *mime) +{ + (void)proxy; + struct primary_offer *o = ud; + const char *match = match_text_mime(mime); + if (match) { + if (!o->mime || !strcasecmp(match, "text/plain;charset=utf-8")) + offer_keep_mime(&o->mime, mime); + } +} + +static const struct zwp_primary_selection_offer_v1_listener primary_offer_listener = { + .offer = primary_offer_mime, +}; + +static void primary_data_offer(void *ud, struct zwp_primary_selection_device_v1 *device, + struct zwp_primary_selection_offer_v1 *proxy) +{ + (void)device; + struct clipboard *c = ud; + struct primary_offer *o = calloc(1, sizeof(*o)); + if (!o) { zwp_primary_selection_offer_v1_destroy(proxy); return; } + o->proxy = proxy; + zwp_primary_selection_offer_v1_add_listener(proxy, &primary_offer_listener, o); + o->next = c->primary_pending_offers; + c->primary_pending_offers = o; +} + +static void primary_selection(void *ud, struct zwp_primary_selection_device_v1 *device, + struct zwp_primary_selection_offer_v1 *proxy) +{ + (void)device; + struct clipboard *c = ud; + struct primary_offer *o = proxy ? zwp_primary_selection_offer_v1_get_user_data(proxy) : NULL; + take_pending_primary_offer(c, o); + if (c->primary_selection != o) free_primary_offer(c->primary_selection); + c->primary_selection = o; +} + +static const struct zwp_primary_selection_device_v1_listener primary_device_listener = { + .data_offer = primary_data_offer, + .selection = primary_selection, +}; + +static void primary_source_send(void *ud, struct zwp_primary_selection_source_v1 *s, + const char *mime, int32_t fd) +{ + (void)s; (void)mime; + struct clipboard *c = ud; + start_transfer_text(c, fd, true, false, c->primary_text); +} + +static void primary_source_cancelled(void *ud, struct zwp_primary_selection_source_v1 *s) +{ + struct clipboard *c = ud; + if (c->primary_source == s) c->primary_source = NULL; + zwp_primary_selection_source_v1_destroy(s); +} + +static const struct zwp_primary_selection_source_v1_listener primary_source_listener = { + .send = primary_source_send, + .cancelled = primary_source_cancelled, +}; + +void clipboard_set_primary_manager(struct clipboard *c, + struct zwp_primary_selection_device_manager_v1 *mgr) +{ + if (!c) return; + c->primary_manager = mgr; + if (c->primary_device) { + zwp_primary_selection_device_v1_destroy(c->primary_device); + c->primary_device = NULL; + } + if (c->primary_manager && c->seat) { + c->primary_device = + zwp_primary_selection_device_manager_v1_get_device(c->primary_manager, c->seat); + if (c->primary_device) + zwp_primary_selection_device_v1_add_listener(c->primary_device, + &primary_device_listener, c); + } +} + +void clipboard_set_drag_handler(struct clipboard *c, clipboard_drag_fn fn, void *ud) +{ + if (!c) return; + c->drag = fn; + c->drag_ud = ud; +} + +void clipboard_set_drop_handler(struct clipboard *c, clipboard_drop_fn fn, void *ud) +{ + if (!c) return; + c->drop = fn; + c->drop_ud = ud; +} + +struct wl_data_device *clipboard_data_device(const struct clipboard *c) +{ + return c ? c->device : NULL; +} + +struct clipboard *clipboard_create(struct wl_display *display, struct wl_data_device_manager *manager, + struct wl_seat *seat, struct yasl_loop *loop, + clipboard_receive_fn receive, void *ud) +{ + if (!manager || !seat) return NULL; + struct clipboard *c = calloc(1, sizeof(*c)); + if (!c) return NULL; + c->display = display; c->manager = manager; c->loop = loop; c->receive = receive; c->ud = ud; + c->device = wl_data_device_manager_get_data_device(manager, seat); + if (!c->device) { free(c); return NULL; } + c->seat = seat; + wl_data_device_add_listener(c->device, &device_listener, c); + return c; +} + +bool clipboard_set_seat(struct clipboard *c, struct wl_seat *seat) +{ + if (!c || c->seat == seat) return false; + c->seat = seat; + drag_offer_clear(c, c->device); + if (c->device) { + wl_data_device_destroy(c->device); + c->device = NULL; + } + free_offer(c->selection); + c->selection = NULL; + clear_pending_offers(c); + if (c->source) { + wl_data_source_destroy(c->source); + c->source = NULL; + } + if (c->primary_device) { + zwp_primary_selection_device_v1_destroy(c->primary_device); + c->primary_device = NULL; + } + free_primary_offer(c->primary_selection); + c->primary_selection = NULL; + clear_pending_primary_offers(c); + if (c->primary_source) { + zwp_primary_selection_source_v1_destroy(c->primary_source); + c->primary_source = NULL; + } + if (!seat) return true; + if (c->manager) { + c->device = wl_data_device_manager_get_data_device(c->manager, seat); + if (c->device) + wl_data_device_add_listener(c->device, &device_listener, c); + } + if (c->primary_manager) { + c->primary_device = + zwp_primary_selection_device_manager_v1_get_device(c->primary_manager, seat); + if (c->primary_device) + zwp_primary_selection_device_v1_add_listener(c->primary_device, + &primary_device_listener, c); + } + return true; +} + +void clipboard_copy(struct clipboard *c, const char *text, uint32_t serial) +{ + if (!c || !c->device || !text || !serial) return; + char *copy = strdup(text); + if (!copy) return; + if (c->source) wl_data_source_destroy(c->source); + free(c->text); c->text = copy; + c->source = wl_data_device_manager_create_data_source(c->manager); + if (!c->source) return; + wl_data_source_add_listener(c->source, &source_listener, c); + wl_data_source_offer(c->source, "text/plain;charset=utf-8"); + wl_data_source_offer(c->source, "text/plain"); + wl_data_source_offer(c->source, "UTF8_STRING"); + wl_data_source_offer(c->source, "STRING"); + wl_data_source_offer(c->source, "TEXT"); + wl_data_device_set_selection(c->device, c->source, serial); +} + +void clipboard_paste(struct clipboard *c, struct wl_surface *target) +{ + if (!c || !c->device || !c->selection || !c->selection->mime) return; + int fds[2]; + if (pipe2(fds, O_CLOEXEC) < 0) return; + wl_data_offer_receive(c->selection->proxy, c->selection->mime, fds[1]); + close(fds[1]); + struct transfer *t = start_transfer(c, fds[0], false, false); + if (t) t->drop_surface = target; + wl_display_flush(c->display); +} + +void clipboard_copy_primary(struct clipboard *c, const char *text, uint32_t serial) +{ + if (!c || !text || !serial) return; + if (!c->primary_device || !c->primary_manager) return; + char *copy = strdup(text); + if (!copy) return; + if (c->primary_source) zwp_primary_selection_source_v1_destroy(c->primary_source); + free(c->primary_text); c->primary_text = copy; + c->primary_source = zwp_primary_selection_device_manager_v1_create_source(c->primary_manager); + if (!c->primary_source) return; + zwp_primary_selection_source_v1_add_listener(c->primary_source, &primary_source_listener, c); + zwp_primary_selection_source_v1_offer(c->primary_source, "text/plain;charset=utf-8"); + zwp_primary_selection_source_v1_offer(c->primary_source, "text/plain"); + zwp_primary_selection_source_v1_offer(c->primary_source, "UTF8_STRING"); + zwp_primary_selection_source_v1_offer(c->primary_source, "STRING"); + zwp_primary_selection_source_v1_offer(c->primary_source, "TEXT"); + zwp_primary_selection_device_v1_set_selection(c->primary_device, c->primary_source, serial); +} + +void clipboard_paste_primary(struct clipboard *c, struct wl_surface *target) +{ + if (!c) return; + if (c->primary_device && c->primary_selection && c->primary_selection->mime) { + int fds[2]; + if (pipe2(fds, O_CLOEXEC) < 0) return; + zwp_primary_selection_offer_v1_receive(c->primary_selection->proxy, + c->primary_selection->mime, fds[1]); + close(fds[1]); + struct transfer *t = start_transfer(c, fds[0], false, false); + if (t) t->drop_surface = target; + wl_display_flush(c->display); + return; + } +} + +void clipboard_forget_surface(struct clipboard *c, struct wl_surface *surface) +{ + if (!c || !surface) return; + for (struct transfer *t = c->transfers, *next; t; t = next) { + next = t->next; + if (t->drop_surface == surface) transfer_destroy(t); + } + if (c->drag_surface == surface) c->drag_surface = NULL; +} + +void clipboard_destroy(struct clipboard *c) +{ + if (!c) return; + while (c->transfers) + transfer_destroy(c->transfers); + free_offer(c->selection); + clear_pending_offers(c); + free_offer(c->drag_offer); + free_primary_offer(c->primary_selection); + clear_pending_primary_offers(c); + if (c->primary_source) zwp_primary_selection_source_v1_destroy(c->primary_source); + if (c->primary_device) zwp_primary_selection_device_v1_destroy(c->primary_device); + free(c->primary_text); + if (c->source) wl_data_source_destroy(c->source); + if (c->device) wl_data_device_destroy(c->device); + free(c->text); free(c); +} |
