From 13589e1db9bb428e5bb5aac45f7e950d898cc5a9 Mon Sep 17 00:00:00 2001 From: Slendi Date: Wed, 26 Nov 2025 19:14:24 +0200 Subject: [PATCH] Fix clipboard being bugged out and not pasting or copying Signed-off-by: Slendi --- CMakeLists.txt | 10 +- protocols/wlr-data-control-unstable-v1.xml | 278 ++++++++++ src/App.cpp | 617 +++++++++++++++++---- src/App.hpp | 29 +- src/ImGui.cpp | 2 +- src/Tick.cpp | 7 +- 6 files changed, 817 insertions(+), 126 deletions(-) create mode 100644 protocols/wlr-data-control-unstable-v1.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 50f5244..3ec68ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -153,18 +153,23 @@ set(WLR_LAYER_SHELL_XML set(TEXT_INPUT_XML "${WAYLAND_PROTOCOLS_DIR}/unstable/text-input/text-input-unstable-v3.xml" ) +set(WLR_DATA_CONTROL_XML + "${CMAKE_CURRENT_SOURCE_DIR}/protocols/wlr-data-control-unstable-v1.xml" +) set(GEN_C_HEADERS "${GEN_DIR}/xdg-shell-client-protocol.h" "${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h" "${GEN_DIR}/ext-background-effect-v1-client-protocol.h" "${GEN_DIR}/text-input-unstable-v3-client-protocol.h" + "${GEN_DIR}/wlr-data-control-unstable-v1-client-protocol.h" ) set(GEN_C_PRIVATES "${GEN_DIR}/xdg-shell-protocol.c" "${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c" "${GEN_DIR}/ext-background-effect-v1-protocol.c" "${GEN_DIR}/text-input-unstable-v3-protocol.c" + "${GEN_DIR}/wlr-data-control-unstable-v1-protocol.c" ) add_custom_command( @@ -180,7 +185,10 @@ add_custom_command( # text-input-unstable-v3 COMMAND "${WAYLAND_SCANNER}" client-header "${TEXT_INPUT_XML}" "${GEN_DIR}/text-input-unstable-v3-client-protocol.h" COMMAND "${WAYLAND_SCANNER}" private-code "${TEXT_INPUT_XML}" "${GEN_DIR}/text-input-unstable-v3-protocol.c" - DEPENDS "${XDG_SHELL_XML}" "${WLR_LAYER_SHELL_XML}" "${TEXT_INPUT_XML}" + # wlr-data-control + COMMAND "${WAYLAND_SCANNER}" client-header "${WLR_DATA_CONTROL_XML}" "${GEN_DIR}/wlr-data-control-unstable-v1-client-protocol.h" + COMMAND "${WAYLAND_SCANNER}" private-code "${WLR_DATA_CONTROL_XML}" "${GEN_DIR}/wlr-data-control-unstable-v1-protocol.c" + DEPENDS "${XDG_SHELL_XML}" "${WLR_LAYER_SHELL_XML}" "${TEXT_INPUT_XML}" "${WLR_DATA_CONTROL_XML}" COMMENT "Generating Wayland + wlr-layer-shell client headers and private code" VERBATIM ) diff --git a/protocols/wlr-data-control-unstable-v1.xml b/protocols/wlr-data-control-unstable-v1.xml new file mode 100644 index 0000000..75e8671 --- /dev/null +++ b/protocols/wlr-data-control-unstable-v1.xml @@ -0,0 +1,278 @@ + + + + Copyright © 2018 Simon Ser + Copyright © 2019 Ivan Molodetskikh + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + This protocol allows a privileged client to control data devices. In + particular, the client will be able to manage the current selection and take + the role of a clipboard manager. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This interface is a manager that allows creating per-seat data device + controls. + + + + + Create a new data source. + + + + + + + Create a data device that can be used to manage a seat's selection. + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + This interface allows a client to manage a seat's selection. + + When the seat is destroyed, this object becomes inert. + + + + + This request asks the compositor to set the selection to the data from + the source on behalf of the client. + + The given source may not be used in any further set_selection or + set_primary_selection requests. Attempting to use a previously used + source is a protocol error. + + To unset the selection, set the source to NULL. + + + + + + + Destroys the data device object. + + + + + + The data_offer event introduces a new wlr_data_control_offer object, + which will subsequently be used in either the + wlr_data_control_device.selection event (for the regular clipboard + selections) or the wlr_data_control_device.primary_selection event (for + the primary clipboard selections). Immediately following the + wlr_data_control_device.data_offer event, the new data_offer object + will send out wlr_data_control_offer.offer events to describe the MIME + types it offers. + + + + + + + The selection event is sent out to notify the client of a new + wlr_data_control_offer for the selection for this device. The + wlr_data_control_device.data_offer and the wlr_data_control_offer.offer + events are sent out immediately before this event to introduce the data + offer object. The selection event is sent to a client when a new + selection is set. The wlr_data_control_offer is valid until a new + wlr_data_control_offer or NULL is received. The client must destroy the + previous selection wlr_data_control_offer, if any, upon receiving this + event. + + The first selection event is sent upon binding the + wlr_data_control_device object. + + + + + + + This data control object is no longer valid and should be destroyed by + the client. + + + + + + + + The primary_selection event is sent out to notify the client of a new + wlr_data_control_offer for the primary selection for this device. The + wlr_data_control_device.data_offer and the wlr_data_control_offer.offer + events are sent out immediately before this event to introduce the data + offer object. The primary_selection event is sent to a client when a + new primary selection is set. The wlr_data_control_offer is valid until + a new wlr_data_control_offer or NULL is received. The client must + destroy the previous primary selection wlr_data_control_offer, if any, + upon receiving this event. + + If the compositor supports primary selection, the first + primary_selection event is sent upon binding the + wlr_data_control_device object. + + + + + + + This request asks the compositor to set the primary selection to the + data from the source on behalf of the client. + + The given source may not be used in any further set_selection or + set_primary_selection requests. Attempting to use a previously used + source is a protocol error. + + To unset the primary selection, set the source to NULL. + + The compositor will ignore this request if it does not support primary + selection. + + + + + + + + + + + + The wlr_data_control_source object is the source side of a + wlr_data_control_offer. It is created by the source client in a data + transfer and provides a way to describe the offered data and a way to + respond to requests to transfer the data. + + + + + + + + + This request adds a MIME type to the set of MIME types advertised to + targets. Can be called several times to offer multiple types. + + Calling this after wlr_data_control_device.set_selection is a protocol + error. + + + + + + + Destroys the data source object. + + + + + + Request for data from the client. Send the data as the specified MIME + type over the passed file descriptor, then close it. + + + + + + + + This data source is no longer valid. The data source has been replaced + by another data source. + + The client should clean up and destroy this data source. + + + + + + + A wlr_data_control_offer represents a piece of data offered for transfer + by another client (the source client). The offer describes the different + MIME types that the data can be converted to and provides the mechanism + for transferring the data directly from the source client. + + + + + To transfer the offered data, the client issues this request and + indicates the MIME type 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 + then closes its end, at which point the transfer is complete. + + This request may happen multiple times for different MIME types. + + + + + + + + Destroys the data offer object. + + + + + + Sent immediately after creating the wlr_data_control_offer object. + One event per offered MIME type. + + + + + diff --git a/src/App.cpp b/src/App.cpp index 59b2f54..aa3ec67 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -233,6 +233,40 @@ App::~App() xkb_keymap_unref(m_kbd.xkb_keymap_v); if (m_kbd.xkb_ctx_v) xkb_context_unref(m_kbd.xkb_ctx_v); + if (m_wayland.dd_source) { + wl_data_source_destroy(m_wayland.dd_source); + m_wayland.dd_source = nullptr; + } + if (m_wayland.dd_offer) { + wl_data_offer_destroy(m_wayland.dd_offer); + m_wayland.dd_offer = nullptr; + } + m_wl_data_offers.clear(); + if (m_wayland.ddev) { + wl_data_device_destroy(m_wayland.ddev); + m_wayland.ddev = nullptr; + } + if (m_wayland.ddm) { + wl_data_device_manager_destroy(m_wayland.ddm); + m_wayland.ddm = nullptr; + } + if (m_wayland.curr_source) { + zwlr_data_control_source_v1_destroy(m_wayland.curr_source); + m_wayland.curr_source = nullptr; + } + for (auto &[offer, _] : m_data_control_offers) { + if (offer) + zwlr_data_control_offer_v1_destroy(offer); + } + m_data_control_offers.clear(); + if (m_wayland.data_control_device) { + zwlr_data_control_device_v1_destroy(m_wayland.data_control_device); + m_wayland.data_control_device = nullptr; + } + if (m_wayland.data_control_mgr) { + zwlr_data_control_manager_v1_destroy(m_wayland.data_control_mgr); + m_wayland.data_control_mgr = nullptr; + } if (m_wayland.text_input) { zwp_text_input_v3_destroy(m_wayland.text_input); m_wayland.text_input = nullptr; @@ -289,6 +323,11 @@ auto App::set_visible(bool visible) -> void auto App::init_wayland() -> void { + m_debug_log = []() { + auto const env = getenv("WAYLIGHT_DEBUG"); + return env && *env; + }(); + m_wayland.display = wl_display_connect(nullptr); if (!m_wayland.display) { std::fprintf(stderr, "failed to connect to Wayland display\n"); @@ -540,10 +579,358 @@ auto App::init_wayland() -> void app->m_ime.sent_serial = 0; } }; + static auto destroy_data_control_offer + = +[](App *app, zwlr_data_control_offer_v1 *offer) { + if (!offer) + return; + zwlr_data_control_offer_v1_destroy(offer); + app->m_data_control_offers.erase(offer); + }; + + static wl_data_offer_listener const dd_offer_listener { + .offer = + [](void *data, wl_data_offer *offer, char const *mime) { + auto *app = static_cast(data); + auto &info = app->m_wl_data_offers[offer]; + auto const is_text_plain = [](char const *m) { + return std::strncmp(m, "text/plain", 10) == 0; + }; + auto const is_text = [](char const *m) { + return std::strncmp(m, "text/", 5) == 0; + }; + + if (std::strcmp(mime, "text/plain;charset=utf-8") == 0) + info.has_utf8 = true; + else if (std::strcmp(mime, "text/plain") == 0) + info.has_text = true; + + if (info.first_text_mime.empty() && mime) { + if (is_text(mime) || is_text_plain(mime) + || std::strcmp(mime, "UTF8_STRING") == 0 + || std::strcmp(mime, "STRING") == 0) { + info.first_text_mime = mime; + } + } + + if (app->m_debug_log) + std::println("wl-data: offer mime {}", mime); + }, +#if WL_DATA_OFFER_SOURCE_ACTIONS_SINCE_VERSION + .source_actions = [](void *, wl_data_offer *, uint32_t) {}, + .action = [](void *, wl_data_offer *, uint32_t) {}, +#endif + }; + + static wl_data_device_listener const dd_listener { + .data_offer = + [](void *data, wl_data_device *, wl_data_offer *offer) { + auto *app = static_cast(data); + wl_data_offer_add_listener(offer, &dd_offer_listener, app); + if (app->m_wayland.dd_offer && app->m_wayland.dd_offer != offer) + wl_data_offer_destroy(app->m_wayland.dd_offer); + app->m_wayland.dd_offer = offer; + app->m_wl_data_offers.emplace(offer, WlDataOfferInfo {}); + }, + .enter = [](void *, wl_data_device *, uint32_t, wl_surface *, wl_fixed_t, + wl_fixed_t, wl_data_offer *) {}, + .leave = [](void *, wl_data_device *) {}, + .motion = [](void *, wl_data_device *, uint32_t, wl_fixed_t, + wl_fixed_t) {}, + .drop = [](void *, wl_data_device *) {}, + .selection = + [](void *data, wl_data_device *, wl_data_offer *offer) { + auto *app = static_cast(data); + if (!offer) { + app->m_clipboard_cache.clear(); + return; + } + + auto it = app->m_wl_data_offers.find(offer); + bool const has_utf8 + = it != app->m_wl_data_offers.end() && it->second.has_utf8; + bool const has_text + = it != app->m_wl_data_offers.end() && it->second.has_text; + std::string const &first_text_mime + = it != app->m_wl_data_offers.end() + ? it->second.first_text_mime + : std::string {}; + + char const *mime = nullptr; + if (has_utf8) { + mime = "text/plain;charset=utf-8"; + } else if (has_text) { + mime = "text/plain"; + } else if (!first_text_mime.empty()) { + mime = first_text_mime.c_str(); + } else { + if (app->m_debug_log) + std::println("wl-data: no usable mime for selection"); + return; + } + + int fds[2]; + if (pipe(fds) != 0) + return; + + wl_data_offer_receive(offer, mime, fds[1]); + wl_display_flush(app->m_wayland.display); + close(fds[1]); + + int rfd = fds[0]; + + std::thread([app, rfd, offer]() { + std::string data; + char buf[4096]; + for (;;) { + ssize_t n = read(rfd, buf, sizeof buf); + if (n > 0) { + data.append(buf, buf + n); + continue; + } + if (n < 0 && errno == EINTR) + continue; + break; + } + close(rfd); + + struct Ctx { + App *app; + wl_data_offer *offer; + std::string data; + }; + auto *ctx = new Ctx { app, offer, std::move(data) }; + + g_main_context_invoke( + nullptr, + +[](gpointer p) -> gboolean { + auto *ctx = static_cast(p); + if (!ctx->data.empty()) { + ctx->app->m_clipboard_cache + = std::move(ctx->data); + if (ctx->app->m_debug_log) + std::println("wl-data: selection received size {}", + ctx->app->m_clipboard_cache.size()); + } + if (ctx->offer == ctx->app->m_wayland.dd_offer) { + wl_data_offer_destroy(ctx->offer); + ctx->app->m_wayland.dd_offer = nullptr; + } + ctx->app->m_wl_data_offers.erase(ctx->offer); + delete ctx; + return G_SOURCE_REMOVE; + }, + ctx); + }).detach(); + }, + }; + + static auto ensure_data_device { +[](App *app) -> bool { + if (!app->m_wayland.ddm || !app->m_wayland.seat || app->m_wayland.ddev) + return false; + app->m_wayland.ddev + = wl_data_device_manager_get_data_device(app->m_wayland.ddm, + app->m_wayland.seat); + if (!app->m_wayland.ddev) + return false; + if (app->m_debug_log) { + std::println("wl-data: created data_device"); + } + wl_data_device_add_listener(app->m_wayland.ddev, &dd_listener, app); + return true; + } }; + + static auto handle_selection_offer + = +[](App *app, zwlr_data_control_offer_v1 *offer) { + if (app->m_wayland.curr_offer + && app->m_wayland.curr_offer != offer) { + destroy_data_control_offer(app, app->m_wayland.curr_offer); + } + app->m_wayland.curr_offer = offer; + + if (!offer) { + if (app->m_debug_log) + std::println("data-control: selection cleared"); + app->m_clipboard_cache.clear(); + return; + } + + auto it = app->m_data_control_offers.find(offer); + bool const has_utf8 + = it != app->m_data_control_offers.end() && it->second.has_utf8; + bool const has_text + = it != app->m_data_control_offers.end() && it->second.has_text; + + std::string const &first_text_mime + = it != app->m_data_control_offers.end() + ? it->second.first_text_mime + : std::string {}; + + char const *mime = nullptr; + if (has_utf8) { + mime = "text/plain;charset=utf-8"; + } else if (has_text) { + mime = "text/plain"; + } else if (!first_text_mime.empty()) { + mime = first_text_mime.c_str(); + } else { + if (app->m_debug_log) + std::println("data-control: no usable mime, skipping"); + return; + } + + if (app->m_debug_log) + std::println("data-control: receive selection mime {}", mime); + int fds[2]; + if (pipe(fds) != 0) + return; + + zwlr_data_control_offer_v1_receive(offer, mime, fds[1]); + wl_display_flush(app->m_wayland.display); + close(fds[1]); + + int rfd = fds[0]; + + std::thread([app, rfd, offer]() { + std::string data; + char buf[4096]; + for (;;) { + ssize_t n = read(rfd, buf, sizeof buf); + if (n > 0) { + data.append(buf, buf + n); + continue; + } + if (n < 0 && errno == EINTR) + continue; + break; + } + close(rfd); + + struct Ctx { + App *app; + zwlr_data_control_offer_v1 *offer; + std::string data; + }; + auto *ctx = new Ctx { app, offer, std::move(data) }; + + g_main_context_invoke( + nullptr, + +[](gpointer p) -> gboolean { + auto *ctx = static_cast(p); + if (!ctx->data.empty()) + ctx->app->m_clipboard_cache + = std::move(ctx->data); + if (ctx->offer + == ctx->app->m_wayland.curr_offer) { + destroy_data_control_offer( + ctx->app, ctx->offer); + ctx->app->m_wayland.curr_offer = nullptr; + } + delete ctx; + return G_SOURCE_REMOVE; + }, + ctx); + }).detach(); + }; + + static zwlr_data_control_offer_v1_listener const data_offer_listener { + .offer = + [](void *data, zwlr_data_control_offer_v1 *offer, char const *mime) { + auto *app = static_cast(data); + auto &info = app->m_data_control_offers[offer]; + auto const is_text_plain = [](char const *m) { + return std::strncmp(m, "text/plain", 10) == 0; + }; + auto const is_text = [](char const *m) { + return std::strncmp(m, "text/", 5) == 0; + }; + + if (std::strcmp(mime, "text/plain;charset=utf-8") == 0) + info.has_utf8 = true; + else if (std::strcmp(mime, "text/plain") == 0) + info.has_text = true; + + if (info.first_text_mime.empty() && mime) { + if (is_text(mime) || is_text_plain(mime) + || std::strcmp(mime, "UTF8_STRING") == 0 + || std::strcmp(mime, "STRING") == 0) { + info.first_text_mime = mime; + } + } + + if (app->m_debug_log) + std::println("data-control: offer mime {}", mime); + }, + }; + + static zwlr_data_control_device_v1_listener const data_device_listener { + .data_offer = + [](void *data, zwlr_data_control_device_v1 *, + zwlr_data_control_offer_v1 *offer) { + auto *app = static_cast(data); + zwlr_data_control_offer_v1_add_listener( + offer, &data_offer_listener, app); + if (app->m_debug_log) + std::println("data-control: new offer {}", static_cast(offer)); + }, + .selection = + [](void *data, zwlr_data_control_device_v1 *, + zwlr_data_control_offer_v1 *offer) { + auto *app = static_cast(data); + handle_selection_offer(app, offer); + }, + .finished = + [](void *data, zwlr_data_control_device_v1 *device) { + auto *app = static_cast(data); + for (auto &[offer, _] : app->m_data_control_offers) { + if (offer) + zwlr_data_control_offer_v1_destroy(offer); + } + app->m_data_control_offers.clear(); + app->m_wayland.curr_offer = nullptr; + if (app->m_wayland.curr_source) { + zwlr_data_control_source_v1_destroy(app->m_wayland.curr_source); + app->m_wayland.curr_source = nullptr; + } + if (app->m_wayland.data_control_device == device) + app->m_wayland.data_control_device = nullptr; + if (app->m_debug_log) + std::println("data-control: device finished"); + zwlr_data_control_device_v1_destroy(device); + }, + .primary_selection = + [](void *data, zwlr_data_control_device_v1 *, + zwlr_data_control_offer_v1 *offer) { + auto *app = static_cast(data); + handle_selection_offer(app, offer); + }, + }; + + static auto ensure_data_control_device { +[](App *app) -> bool { + if (!app->m_wayland.data_control_mgr || !app->m_wayland.seat + || app->m_wayland.data_control_device) + return false; + app->m_wayland.data_control_device + = zwlr_data_control_manager_v1_get_data_device( + app->m_wayland.data_control_mgr, app->m_wayland.seat); + if (!app->m_wayland.data_control_device) + return false; + if (app->m_debug_log) { + std::println("data-control: created device (manager {}, seat present {})", + static_cast(app->m_wayland.data_control_mgr), true); + } + zwlr_data_control_device_v1_add_listener( + app->m_wayland.data_control_device, &data_device_listener, app); + return true; + } }; + auto handle_registry_global = [](void *data, wl_registry *registry, u32 name, char const *interface, u32 version) -> void { auto *app { static_cast(data) }; + if (app->m_debug_log) { + std::println( + "registry: interface={} name={} version={}", interface, name, version); + } if (std::strcmp(interface, wl_compositor_interface.name) == 0) { app->m_wayland.compositor = static_cast( wl_registry_bind(registry, name, &wl_compositor_interface, 4)); @@ -561,6 +948,11 @@ auto App::init_wayland() -> void app->m_ime.seat_focus = false; } ensure_text_input(app); + bool const created_ctrl + = ensure_data_control_device(app); + bool const created_dd = ensure_data_device(app); + if (created_ctrl || created_dd) + wl_display_roundtrip(app->m_wayland.display); }, .name = [](void *, struct wl_seat *, char const *) {}, }; @@ -585,112 +977,29 @@ auto App::init_wayland() -> void registry, name, &zwp_text_input_manager_v3_interface, 1)); app->m_ime.supported = true; ensure_text_input(app); + } else if (std::strcmp(interface, + zwlr_data_control_manager_v1_interface.name) + == 0) { + if (app->m_debug_log) { + std::println( + "registry: data-control name={} version={}", name, version); + } + uint32_t const bind_version = std::min(version, 2); + app->m_wayland.data_control_mgr + = static_cast(wl_registry_bind( + registry, name, &zwlr_data_control_manager_v1_interface, + bind_version)); + bool const created = ensure_data_control_device(app); + if (created) + wl_display_roundtrip(app->m_wayland.display); } else if (std::strcmp(interface, wl_data_device_manager_interface.name) == 0) { - app->m_wayland.ddm - = static_cast(wl_registry_bind( - registry, name, &wl_data_device_manager_interface, + app->m_wayland.ddm = static_cast( + wl_registry_bind(registry, name, &wl_data_device_manager_interface, std::min(version, 3))); - if (app->m_wayland.ddm && !app->m_wayland.ddev) { - app->m_wayland.ddev = wl_data_device_manager_get_data_device( - app->m_wayland.ddm, app->m_wayland.seat); - static wl_data_device_listener const ddev_l = { - .data_offer = - [](void *data, wl_data_device *, wl_data_offer *offer) { - auto *app = static_cast(data); - static wl_data_offer_listener const offer_l = { - .offer = - [](void *data, wl_data_offer *, - char const *mime) { - auto *app = static_cast(data); - (void)app; - (void)mime; - }, -#if WL_DATA_OFFER_SOURCE_ACTIONS_SINCE_VERSION - .source_actions - = [](void *, wl_data_offer *, uint32_t) {}, - .action - = [](void *, wl_data_offer *, uint32_t) {} -#endif - }; - wl_data_offer_add_listener(offer, &offer_l, app); - if (app->m_wayland.curr_offer - && app->m_wayland.curr_offer != offer) - wl_data_offer_destroy( - app->m_wayland.curr_offer); - app->m_wayland.curr_offer = offer; - }, - .enter - = [](void *, wl_data_device *, uint32_t, wl_surface *, - wl_fixed_t, wl_fixed_t, wl_data_offer *) {}, - .leave = [](void *, wl_data_device *) {}, - .motion = [](void *, wl_data_device *, uint32_t, wl_fixed_t, - wl_fixed_t) {}, - .drop = [](void *, wl_data_device *) {}, - .selection = - [](void *data, wl_data_device *, wl_data_offer *offer) { - auto *app = static_cast(data); - if (!offer) { - app->m_clipboard_cache.clear(); - return; - } - - char const *mime = "text/plain;charset=utf-8"; - int fds[2]; - if (pipe(fds) != 0) - return; - - wl_data_offer_receive(offer, mime, fds[1]); - wl_display_flush(app->m_wayland.display); - close(fds[1]); - - int rfd = fds[0]; - - std::thread([app, rfd, offer]() { - std::string data; - char buf[4096]; - for (;;) { - ssize_t n = read(rfd, buf, sizeof buf); - if (n > 0) { - data.append(buf, buf + n); - continue; - } - if (n < 0 && errno == EINTR) - continue; - break; - } - close(rfd); - - struct Ctx { - App *app; - wl_data_offer *offer; - std::string data; - }; - auto *ctx - = new Ctx { app, offer, std::move(data) }; - - g_main_context_invoke( - nullptr, - +[](gpointer p) -> gboolean { - auto *ctx = static_cast(p); - if (!ctx->data.empty()) - ctx->app->m_clipboard_cache - = std::move(ctx->data); - if (ctx->offer - == ctx->app->m_wayland.curr_offer) { - wl_data_offer_destroy(ctx->offer); - ctx->app->m_wayland.curr_offer - = nullptr; - } - delete ctx; - return G_SOURCE_REMOVE; - }, - ctx); - }).detach(); - }, - }; - wl_data_device_add_listener(app->m_wayland.ddev, &ddev_l, app); - } + bool const created = ensure_data_device(app); + if (created) + wl_display_roundtrip(app->m_wayland.display); } }; @@ -705,6 +1014,14 @@ auto App::init_wayland() -> void m_kbd.xkb_ctx_v = xkb_context_new(XKB_CONTEXT_NO_FLAGS); wl_display_roundtrip(m_wayland.display); + if (m_debug_log) { + std::println("data-control: mgr={} device={}", + static_cast(m_wayland.data_control_mgr), + static_cast(m_wayland.data_control_device)); + std::println("wl-data: ddm={} ddev={}", + static_cast(m_wayland.ddm), + static_cast(m_wayland.ddev)); + } create_layer_surface(); } @@ -1177,19 +1494,87 @@ auto App::pump_events() -> void } } -auto App::clipboard(std::string_view const &str) -> void +auto App::set_clipboard(std::string_view const &str) -> void { + m_clipboard_cache.assign(str.begin(), str.end()); + + if (m_wayland.data_control_mgr && m_wayland.data_control_device) { + if (m_debug_log) + std::println("data-control: set_clipboard size {}", str.size()); + + if (m_wayland.curr_source) { + zwlr_data_control_source_v1_destroy(m_wayland.curr_source); + m_wayland.curr_source = nullptr; + } + + m_wayland.curr_source = zwlr_data_control_manager_v1_create_data_source( + m_wayland.data_control_mgr); + + static zwlr_data_control_source_v1_listener const src_l = { + .send = + [](void *data, zwlr_data_control_source_v1 *, char const *, + int32_t fd) { + auto *app = static_cast(data); + + int wfd = dup(fd); + close(fd); + + std::pmr::string payload = app->m_clipboard_cache; + std::thread([wfd, payload = std::move(payload)]() { + size_t off = 0; + while (off < payload.size()) { + ssize_t n = write(wfd, payload.data() + off, + std::min( + 64 * 1024, payload.size() - off)); + if (n > 0) { + off += (size_t)n; + continue; + } + if (n < 0 && (errno == EINTR)) + continue; + if (n < 0 && (errno == EAGAIN)) { + std::this_thread::sleep_for( + std::chrono::milliseconds(1)); + continue; + } + break; + } + close(wfd); + }).detach(); + }, + .cancelled = + [](void *data, zwlr_data_control_source_v1 *src) { + auto *app = static_cast(data); + if (app->m_wayland.curr_source == src) + app->m_wayland.curr_source = nullptr; + zwlr_data_control_source_v1_destroy(src); + }, + }; + zwlr_data_control_source_v1_add_listener( + m_wayland.curr_source, &src_l, this); + zwlr_data_control_source_v1_offer( + m_wayland.curr_source, "text/plain;charset=utf-8"); + zwlr_data_control_source_v1_offer(m_wayland.curr_source, "text/plain"); + + zwlr_data_control_device_v1_set_selection( + m_wayland.data_control_device, m_wayland.curr_source); + wl_display_flush(m_wayland.display); + return; + } + if (!m_wayland.ddm || !m_wayland.ddev || !m_wayland.seat) return; if (m_last_serial == 0) return; - if (m_wayland.curr_source) { - wl_data_source_destroy(m_wayland.curr_source); - m_wayland.curr_source = nullptr; + if (m_debug_log) + std::println("wl-data: set_clipboard size {}", str.size()); + + if (m_wayland.dd_source) { + wl_data_source_destroy(m_wayland.dd_source); + m_wayland.dd_source = nullptr; } - m_wayland.curr_source - = wl_data_device_manager_create_data_source(m_wayland.ddm); + m_wayland.dd_source = wl_data_device_manager_create_data_source(m_wayland.ddm); static wl_data_source_listener const src_l = { .target = [](void *, wl_data_source *, char const *) {}, @@ -1225,8 +1610,8 @@ auto App::clipboard(std::string_view const &str) -> void .cancelled = [](void *data, wl_data_source *src) { auto *app = static_cast(data); - if (app->m_wayland.curr_source == src) - app->m_wayland.curr_source = nullptr; + if (app->m_wayland.dd_source == src) + app->m_wayland.dd_source = nullptr; wl_data_source_destroy(src); }, #if WL_DATA_SOURCE_DND_DROP_PERFORMED_SINCE_VERSION @@ -1235,14 +1620,12 @@ auto App::clipboard(std::string_view const &str) -> void .action = [](void *, wl_data_source *, uint32_t) {} #endif }; - wl_data_source_add_listener(m_wayland.curr_source, &src_l, this); - wl_data_source_offer(m_wayland.curr_source, "text/plain;charset=utf-8"); - wl_data_source_offer(m_wayland.curr_source, "text/plain"); - - m_clipboard_cache.assign(str.begin(), str.end()); + wl_data_source_add_listener(m_wayland.dd_source, &src_l, this); + wl_data_source_offer(m_wayland.dd_source, "text/plain;charset=utf-8"); + wl_data_source_offer(m_wayland.dd_source, "text/plain"); wl_data_device_set_selection( - m_wayland.ddev, m_wayland.curr_source, m_last_serial); + m_wayland.ddev, m_wayland.dd_source, m_last_serial); wl_display_flush(m_wayland.display); } diff --git a/src/App.hpp b/src/App.hpp index 5bfd10c..08e168c 100644 --- a/src/App.hpp +++ b/src/App.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -14,6 +15,7 @@ extern "C" { #include "ext-background-effect-v1-client-protocol.h" #include "text-input-unstable-v3-client-protocol.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h" +#include "wlr-data-control-unstable-v1-client-protocol.h" #include #undef namespace } @@ -65,15 +67,26 @@ private: return m_themes[m_active_theme]; } - auto clipboard() const -> std::pmr::string const & + auto get_clipboard() const -> std::pmr::string const & { return m_clipboard_cache; } - auto clipboard(std::string_view const &str) -> void; + auto set_clipboard(std::string_view const &str) -> void; static void on_settings_changed(XdpSettings * /*self*/, char const *ns, char const *key, GVariant * /*value*/, gpointer data); + struct DataControlOfferInfo { + bool has_utf8 { false }; + bool has_text { false }; + std::string first_text_mime; + }; + struct WlDataOfferInfo { + bool has_utf8 { false }; + bool has_text { false }; + std::string first_text_mime; + }; + struct { wl_display *display {}; wl_registry *registry {}; @@ -89,11 +102,19 @@ private: zwp_text_input_v3 *text_input {}; wl_data_device_manager *ddm {}; wl_data_device *ddev {}; - wl_data_offer *curr_offer {}; - wl_data_source *curr_source {}; + wl_data_offer *dd_offer {}; + wl_data_source *dd_source {}; + zwlr_data_control_manager_v1 *data_control_mgr {}; + zwlr_data_control_device_v1 *data_control_device {}; + zwlr_data_control_offer_v1 *curr_offer {}; + zwlr_data_control_source_v1 *curr_source {}; } m_wayland; std::pmr::string m_clipboard_cache; + std::unordered_map + m_data_control_offers; + std::unordered_map m_wl_data_offers; u32 m_last_serial { 0 }; + bool m_debug_log { false }; struct { EGLDisplay edpy { EGL_NO_DISPLAY }; diff --git a/src/ImGui.cpp b/src/ImGui.cpp index ca29c92..c61ca2b 100644 --- a/src/ImGui.cpp +++ b/src/ImGui.cpp @@ -543,7 +543,7 @@ auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, std::string clip2; clip2.reserve(m_clipboard.size()); std::copy_if(m_clipboard.begin(), m_clipboard.end(), - clip2.begin(), + std::back_inserter(clip2), [](char ch) { return ch != '\n' && ch != '\r'; }); str.insert(caret_byte, clip2); state.current_rune_idx += (int)utf8_length(clip2); diff --git a/src/Tick.cpp b/src/Tick.cpp index 0abf8b6..4839f6f 100644 --- a/src/Tick.cpp +++ b/src/Tick.cpp @@ -1,6 +1,7 @@ #include "App.hpp" #include +#include #include #include @@ -51,8 +52,8 @@ auto App::tick() -> void m_kbd.typing.clear(); } ImGuiGuard gui_scope(m_gui, rune, m_kbd.ctrl(), m_kbd.shift(), - clipboard(), - [this](std::string_view const &str) { clipboard(str); }); + get_clipboard(), + [this](std::string_view const &str) { set_clipboard(str); }); Rectangle const input_rect { 0.0f, @@ -60,7 +61,7 @@ auto App::tick() -> void static_cast(GetScreenWidth()), static_cast(GetScreenHeight()), }; - ; + if (auto const result = m_gui->text_input(1, text_input_data, input_rect); result.test(1)) {