2026-05-12 00:24:48 +02:00
|
|
|
|
/*
|
|
|
|
|
|
Crafter®.Graphics
|
|
|
|
|
|
Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
Catcrafts.net
|
|
|
|
|
|
|
|
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
|
|
|
|
modify it under the terms of the GNU Lesser General Public
|
|
|
|
|
|
License as published by the Free Software Foundation; either
|
|
|
|
|
|
version 3.0 of the License, or (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
|
|
Lesser General Public License for more details.
|
|
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
|
|
|
|
License along with this library; if not, write to the Free Software
|
|
|
|
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
|
|
*/
|
|
|
|
|
|
module;
|
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
|
|
|
|
#include <wayland-client.h>
|
|
|
|
|
|
#include <wayland-client-protocol.h>
|
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
#include <errno.h>
|
2026-05-19 00:45:22 +02:00
|
|
|
|
#include <poll.h>
|
|
|
|
|
|
#include <fcntl.h>
|
2026-05-12 00:24:48 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
|
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
|
|
|
|
#define NOMINMAX
|
|
|
|
|
|
#include <windows.h>
|
|
|
|
|
|
#endif
|
|
|
|
|
|
module Crafter.Graphics;
|
|
|
|
|
|
import std;
|
|
|
|
|
|
|
|
|
|
|
|
using namespace Crafter;
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
|
|
|
|
namespace {
|
2026-05-19 00:45:22 +02:00
|
|
|
|
// ─── write path ──────────────────────────────────────────────────
|
|
|
|
|
|
//
|
2026-05-12 00:24:48 +02:00
|
|
|
|
// Heap-allocated state attached to each wl_data_source via its
|
|
|
|
|
|
// user_data slot. The text must outlive the source (the compositor
|
|
|
|
|
|
// can call `send` minutes after we set the selection, every time a
|
|
|
|
|
|
// remote app pastes), so we own it here and free it from
|
|
|
|
|
|
// `OnSourceCancelled` — which is the only callback the compositor
|
|
|
|
|
|
// guarantees will fire when the source is no longer needed (when
|
|
|
|
|
|
// the selection is replaced, or on app exit).
|
|
|
|
|
|
struct Held {
|
|
|
|
|
|
std::string text;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
void OnSourceTarget(void*, wl_data_source*, const char*) {}
|
|
|
|
|
|
|
|
|
|
|
|
void OnSourceSend(void* data, wl_data_source*,
|
|
|
|
|
|
const char* /*mime_type*/, std::int32_t fd) {
|
|
|
|
|
|
// We only ever advertise text MIME types, so any negotiated
|
|
|
|
|
|
// type maps to the same UTF-8 buffer. `send` may fire multiple
|
|
|
|
|
|
// times across the lifetime of one selection (each paste is a
|
|
|
|
|
|
// fresh fd), so we must not consume `text` here.
|
|
|
|
|
|
Held* h = static_cast<Held*>(data);
|
|
|
|
|
|
const char* p = h->text.data();
|
|
|
|
|
|
std::size_t rem = h->text.size();
|
|
|
|
|
|
while (rem > 0) {
|
|
|
|
|
|
const ssize_t w = ::write(fd, p, rem);
|
|
|
|
|
|
if (w < 0) {
|
|
|
|
|
|
if (errno == EINTR) continue;
|
|
|
|
|
|
break; // pipe closed; remote gave up reading
|
|
|
|
|
|
}
|
|
|
|
|
|
if (w == 0) break;
|
|
|
|
|
|
p += w;
|
|
|
|
|
|
rem -= static_cast<std::size_t>(w);
|
|
|
|
|
|
}
|
|
|
|
|
|
::close(fd);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnSourceCancelled(void* data, wl_data_source* source) {
|
|
|
|
|
|
// Selection was replaced (someone else copied) or the app is
|
|
|
|
|
|
// exiting. Either way, this is our hook to release the held
|
|
|
|
|
|
// text and the source itself.
|
|
|
|
|
|
delete static_cast<Held*>(data);
|
|
|
|
|
|
wl_data_source_destroy(source);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Drag-and-drop only callbacks. We never start a DnD action, so
|
|
|
|
|
|
// these can't fire — but the listener struct's size is fixed at
|
|
|
|
|
|
// the v3 shape, and wayland-client uses the struct's slots
|
|
|
|
|
|
// directly. Stubs are required.
|
|
|
|
|
|
void OnSourceDndDropPerformed(void*, wl_data_source*) {}
|
|
|
|
|
|
void OnSourceDndFinished(void*, wl_data_source*) {}
|
|
|
|
|
|
void OnSourceAction(void*, wl_data_source*, std::uint32_t) {}
|
|
|
|
|
|
|
|
|
|
|
|
constexpr wl_data_source_listener kSourceListener = {
|
|
|
|
|
|
.target = OnSourceTarget,
|
|
|
|
|
|
.send = OnSourceSend,
|
|
|
|
|
|
.cancelled = OnSourceCancelled,
|
|
|
|
|
|
.dnd_drop_performed = OnSourceDndDropPerformed,
|
|
|
|
|
|
.dnd_finished = OnSourceDndFinished,
|
|
|
|
|
|
.action = OnSourceAction,
|
|
|
|
|
|
};
|
2026-05-19 00:45:22 +02:00
|
|
|
|
|
|
|
|
|
|
// ─── read path ───────────────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// The compositor announces every clipboard selection (and every
|
|
|
|
|
|
// drag-and-drop) via a `wl_data_offer` we have to register a
|
|
|
|
|
|
// listener on to learn its advertised mime types. The lifecycle:
|
|
|
|
|
|
//
|
|
|
|
|
|
// 1. wl_data_device.data_offer → fresh wl_data_offer ID. We
|
|
|
|
|
|
// attach an offer listener so we receive the mime list.
|
|
|
|
|
|
// 2. wl_data_offer.offer (N×) → one event per advertised mime.
|
|
|
|
|
|
// 3. wl_data_device.selection → the offer becomes "the
|
|
|
|
|
|
// clipboard". Replaces (and invalidates) the previous one.
|
|
|
|
|
|
// An offer parameter of nullptr means the clipboard was
|
|
|
|
|
|
// cleared.
|
|
|
|
|
|
//
|
|
|
|
|
|
// DnD offers travel through the same data_offer event and end up
|
|
|
|
|
|
// delivered via `enter`/`leave`/`drop`. We don't implement DnD,
|
|
|
|
|
|
// but we still need to destroy those offers so we don't leak the
|
|
|
|
|
|
// mime vector attached to them — we stash the most recent DnD
|
|
|
|
|
|
// offer on enter and destroy it on leave/drop.
|
|
|
|
|
|
struct OfferState {
|
|
|
|
|
|
std::vector<std::string> mimes;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Map keyed on raw wl_data_offer pointer. The compositor hands the
|
|
|
|
|
|
// same pointer back in subsequent events, so identity is stable.
|
|
|
|
|
|
std::unordered_map<wl_data_offer*, OfferState> g_offers;
|
|
|
|
|
|
wl_data_offer* g_selectionOffer = nullptr;
|
|
|
|
|
|
wl_data_offer* g_dndOffer = nullptr;
|
|
|
|
|
|
bool g_listenerAttached = false;
|
|
|
|
|
|
|
|
|
|
|
|
void DestroyOffer(wl_data_offer* offer) {
|
|
|
|
|
|
if (offer == nullptr) return;
|
|
|
|
|
|
g_offers.erase(offer);
|
|
|
|
|
|
wl_data_offer_destroy(offer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnOfferOffer(void*, wl_data_offer* offer, const char* mime) {
|
|
|
|
|
|
auto it = g_offers.find(offer);
|
|
|
|
|
|
if (it != g_offers.end()) it->second.mimes.emplace_back(mime);
|
|
|
|
|
|
}
|
|
|
|
|
|
void OnOfferSourceActions(void*, wl_data_offer*, std::uint32_t) {}
|
|
|
|
|
|
void OnOfferAction(void*, wl_data_offer*, std::uint32_t) {}
|
|
|
|
|
|
|
|
|
|
|
|
constexpr wl_data_offer_listener kOfferListener = {
|
|
|
|
|
|
.offer = OnOfferOffer,
|
|
|
|
|
|
.source_actions = OnOfferSourceActions,
|
|
|
|
|
|
.action = OnOfferAction,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
void OnDeviceDataOffer(void*, wl_data_device*, wl_data_offer* offer) {
|
|
|
|
|
|
// First event in the sequence — register so we hear the mime list.
|
|
|
|
|
|
// The offer can still end up unused (DnD that doesn't enter our
|
|
|
|
|
|
// surface) but we always destroy it later in selection or
|
|
|
|
|
|
// leave/drop.
|
|
|
|
|
|
g_offers.emplace(offer, OfferState{});
|
|
|
|
|
|
wl_data_offer_add_listener(offer, &kOfferListener, nullptr);
|
|
|
|
|
|
}
|
|
|
|
|
|
void OnDeviceEnter(void*, wl_data_device*, std::uint32_t,
|
|
|
|
|
|
wl_surface*, wl_fixed_t, wl_fixed_t,
|
|
|
|
|
|
wl_data_offer* offer) {
|
|
|
|
|
|
// DnD started over our surface. We never accept, so we just
|
|
|
|
|
|
// hold the offer until leave/drop and free it then.
|
|
|
|
|
|
g_dndOffer = offer;
|
|
|
|
|
|
}
|
|
|
|
|
|
void OnDeviceLeave(void*, wl_data_device*) {
|
|
|
|
|
|
DestroyOffer(g_dndOffer);
|
|
|
|
|
|
g_dndOffer = nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
void OnDeviceMotion(void*, wl_data_device*, std::uint32_t,
|
|
|
|
|
|
wl_fixed_t, wl_fixed_t) {}
|
|
|
|
|
|
void OnDeviceDrop(void*, wl_data_device*) {
|
|
|
|
|
|
DestroyOffer(g_dndOffer);
|
|
|
|
|
|
g_dndOffer = nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
void OnDeviceSelection(void*, wl_data_device*, wl_data_offer* offer) {
|
|
|
|
|
|
// Selection changed; previous selection offer is invalid.
|
|
|
|
|
|
// `offer == nullptr` means the clipboard was cleared.
|
|
|
|
|
|
DestroyOffer(g_selectionOffer);
|
|
|
|
|
|
g_selectionOffer = offer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
constexpr wl_data_device_listener kDeviceListener = {
|
|
|
|
|
|
.data_offer = OnDeviceDataOffer,
|
|
|
|
|
|
.enter = OnDeviceEnter,
|
|
|
|
|
|
.leave = OnDeviceLeave,
|
|
|
|
|
|
.motion = OnDeviceMotion,
|
|
|
|
|
|
.drop = OnDeviceDrop,
|
|
|
|
|
|
.selection = OnDeviceSelection,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const char* PickTextMime(const OfferState& s) {
|
|
|
|
|
|
// Same priority order we advertise from SetText, so a round-trip
|
|
|
|
|
|
// through ourselves picks the richest mime first.
|
|
|
|
|
|
constexpr std::array kWantedMimes = {
|
|
|
|
|
|
"text/plain;charset=utf-8",
|
|
|
|
|
|
"text/plain",
|
|
|
|
|
|
"UTF8_STRING",
|
|
|
|
|
|
"TEXT",
|
|
|
|
|
|
};
|
|
|
|
|
|
for (const char* want : kWantedMimes) {
|
|
|
|
|
|
for (const auto& m : s.mimes) {
|
|
|
|
|
|
if (m == want) return want;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
2026-05-12 00:24:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2026-05-18 02:07:48 +02:00
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
|
|
|
|
|
// Single JS import — the env.js implementation calls navigator.clipboard.writeText.
|
|
|
|
|
|
// `bool` return mirrors the native paths so callers don't need to #ifdef the
|
|
|
|
|
|
// check; failure cases (no permission, async rejection) just log to console.
|
|
|
|
|
|
// Lives in a named namespace (anonymous would give internal linkage and the
|
|
|
|
|
|
// import_module attribute would be dropped at link time).
|
|
|
|
|
|
namespace Crafter::DomEnv {
|
|
|
|
|
|
__attribute__((import_module("env"), import_name("clipboardSetText")))
|
|
|
|
|
|
bool DomClipboardSetText(const char* text, std::int32_t len);
|
2026-05-19 00:45:22 +02:00
|
|
|
|
|
|
|
|
|
|
// JS returns either 0 (no text latched) or a wasm-malloc'd NUL-
|
|
|
|
|
|
// terminated UTF-8 pointer that we own and free. Matches the
|
|
|
|
|
|
// existing `getValue` ABI used by HtmlElementPtr::GetValue.
|
|
|
|
|
|
__attribute__((import_module("env"), import_name("clipboardGetText")))
|
|
|
|
|
|
const char* DomClipboardGetText();
|
2026-05-18 02:07:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2026-05-19 00:45:22 +02:00
|
|
|
|
void Crafter::Clipboard::Initialize() {
|
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
|
|
|
|
if (g_listenerAttached) return;
|
|
|
|
|
|
if (Device::dataDevice == nullptr) return; // compositor doesn't expose it
|
|
|
|
|
|
wl_data_device_add_listener(Device::dataDevice, &kDeviceListener, nullptr);
|
|
|
|
|
|
g_listenerAttached = true;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 00:24:48 +02:00
|
|
|
|
bool Crafter::Clipboard::SetText(std::string_view text) {
|
2026-05-18 02:07:48 +02:00
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
|
|
|
|
|
return Crafter::DomEnv::DomClipboardSetText(text.data(), static_cast<std::int32_t>(text.size()));
|
|
|
|
|
|
#elif defined(CRAFTER_GRAPHICS_WINDOW_WAYLAND)
|
2026-05-12 00:24:48 +02:00
|
|
|
|
if (Device::dataDeviceManager == nullptr || Device::dataDevice == nullptr) {
|
|
|
|
|
|
// Compositor doesn't expose wl_data_device_manager (rare; some
|
|
|
|
|
|
// headless / minimal compositors). Caller can fall back.
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// wl_data_device.set_selection requires a serial from a recent
|
|
|
|
|
|
// input event. We track the most recent pointer-enter serial on
|
|
|
|
|
|
// each window — fine for "user clicked Copy" interactions, since
|
|
|
|
|
|
// the click itself produced the serial. Without one, the
|
|
|
|
|
|
// compositor would silently reject the request.
|
|
|
|
|
|
std::uint32_t serial = 0;
|
|
|
|
|
|
if (Device::focusedWindow != nullptr) {
|
|
|
|
|
|
serial = Device::focusedWindow->lastPointerSerial_;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (serial == 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
auto* held = new Held{ std::string(text) };
|
|
|
|
|
|
wl_data_source* source =
|
|
|
|
|
|
wl_data_device_manager_create_data_source(Device::dataDeviceManager);
|
|
|
|
|
|
if (source == nullptr) {
|
|
|
|
|
|
delete held;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
wl_data_source_add_listener(source, &kSourceListener, held);
|
|
|
|
|
|
|
|
|
|
|
|
// Advertise the four common text MIME types so legacy + modern
|
|
|
|
|
|
// pasters both find a match. Wayland clients prefer the first one
|
|
|
|
|
|
// they recognise, in order.
|
|
|
|
|
|
wl_data_source_offer(source, "text/plain;charset=utf-8");
|
|
|
|
|
|
wl_data_source_offer(source, "text/plain");
|
|
|
|
|
|
wl_data_source_offer(source, "UTF8_STRING");
|
|
|
|
|
|
wl_data_source_offer(source, "TEXT");
|
|
|
|
|
|
|
|
|
|
|
|
wl_data_device_set_selection(Device::dataDevice, source, serial);
|
|
|
|
|
|
// Push the request so the compositor sees it before the next event
|
|
|
|
|
|
// loop iteration — otherwise a quick read in another app might
|
|
|
|
|
|
// miss the selection update.
|
|
|
|
|
|
wl_display_flush(Device::display);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
#elif defined(CRAFTER_GRAPHICS_WINDOW_WIN32)
|
|
|
|
|
|
// CF_UNICODETEXT round-trip. Convert UTF-8 → UTF-16, allocate a
|
|
|
|
|
|
// moveable HGLOBAL, hand it off to the OS. The OS frees the global
|
|
|
|
|
|
// when the next clipboard owner replaces the data, so our caller
|
|
|
|
|
|
// doesn't need to keep `text` alive — same lifetime contract as
|
|
|
|
|
|
// the Wayland path.
|
|
|
|
|
|
if (!OpenClipboard(nullptr)) return false;
|
|
|
|
|
|
EmptyClipboard();
|
|
|
|
|
|
const int wlen = MultiByteToWideChar(CP_UTF8, 0,
|
|
|
|
|
|
text.data(), static_cast<int>(text.size()), nullptr, 0);
|
|
|
|
|
|
if (wlen < 0) { CloseClipboard(); return false; }
|
|
|
|
|
|
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE,
|
|
|
|
|
|
(static_cast<std::size_t>(wlen) + 1) * sizeof(wchar_t));
|
|
|
|
|
|
if (h == nullptr) { CloseClipboard(); return false; }
|
|
|
|
|
|
wchar_t* dst = static_cast<wchar_t*>(GlobalLock(h));
|
|
|
|
|
|
if (dst == nullptr) {
|
|
|
|
|
|
GlobalFree(h);
|
|
|
|
|
|
CloseClipboard();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
MultiByteToWideChar(CP_UTF8, 0,
|
|
|
|
|
|
text.data(), static_cast<int>(text.size()), dst, wlen);
|
|
|
|
|
|
dst[wlen] = L'\0';
|
|
|
|
|
|
GlobalUnlock(h);
|
|
|
|
|
|
const bool ok = SetClipboardData(CF_UNICODETEXT, h) != nullptr;
|
|
|
|
|
|
if (!ok) GlobalFree(h); // OS only takes ownership on success
|
|
|
|
|
|
CloseClipboard();
|
|
|
|
|
|
return ok;
|
|
|
|
|
|
#else
|
|
|
|
|
|
(void)text;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
2026-05-19 00:45:22 +02:00
|
|
|
|
|
|
|
|
|
|
std::optional<std::string> Crafter::Clipboard::GetText() {
|
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
|
|
|
|
|
const char* raw = Crafter::DomEnv::DomClipboardGetText();
|
|
|
|
|
|
if (raw == nullptr) return std::nullopt;
|
|
|
|
|
|
std::string out(raw);
|
|
|
|
|
|
std::free(const_cast<char*>(raw)); // JS side allocated via WasmAlloc/malloc
|
|
|
|
|
|
return out;
|
|
|
|
|
|
#elif defined(CRAFTER_GRAPHICS_WINDOW_WAYLAND)
|
|
|
|
|
|
if (g_selectionOffer == nullptr) return std::nullopt;
|
|
|
|
|
|
auto it = g_offers.find(g_selectionOffer);
|
|
|
|
|
|
if (it == g_offers.end()) return std::nullopt;
|
|
|
|
|
|
const char* mime = PickTextMime(it->second);
|
|
|
|
|
|
if (mime == nullptr) return std::nullopt; // selection holds no text mime
|
|
|
|
|
|
|
|
|
|
|
|
int fds[2];
|
|
|
|
|
|
if (::pipe(fds) != 0) return std::nullopt;
|
|
|
|
|
|
// Read end stays blocking; we drive it with poll() below.
|
|
|
|
|
|
|
|
|
|
|
|
wl_data_offer_receive(g_selectionOffer, mime, fds[1]);
|
|
|
|
|
|
// Flush so the compositor forwards `receive` to the source app.
|
|
|
|
|
|
wl_display_flush(Device::display);
|
|
|
|
|
|
// Drop our copy of the write end. The source app's copy stays open
|
|
|
|
|
|
// until it finishes writing; once it closes, our read sees EOF.
|
|
|
|
|
|
::close(fds[1]);
|
|
|
|
|
|
|
|
|
|
|
|
std::string out;
|
|
|
|
|
|
const int wlFd = wl_display_get_fd(Device::display);
|
|
|
|
|
|
|
|
|
|
|
|
// One-second budget. The poll loop also drives the Wayland event
|
|
|
|
|
|
// pump so that if *we* are the source of the selection, our own
|
|
|
|
|
|
// data_source.send callback can run and write into the pipe — a
|
|
|
|
|
|
// plain blocking read on the pipe would deadlock in that case.
|
|
|
|
|
|
constexpr int kTimeoutMs = 1000;
|
|
|
|
|
|
auto deadline = std::chrono::steady_clock::now()
|
|
|
|
|
|
+ std::chrono::milliseconds(kTimeoutMs);
|
|
|
|
|
|
|
|
|
|
|
|
bool done = false;
|
|
|
|
|
|
while (!done) {
|
|
|
|
|
|
// Prepare for a non-blocking read on the wayland fd. The
|
|
|
|
|
|
// prepare/read pair lets us safely interleave with poll().
|
|
|
|
|
|
while (wl_display_prepare_read(Device::display) != 0) {
|
|
|
|
|
|
wl_display_dispatch_pending(Device::display);
|
|
|
|
|
|
}
|
|
|
|
|
|
wl_display_flush(Device::display);
|
|
|
|
|
|
|
|
|
|
|
|
const auto now = std::chrono::steady_clock::now();
|
|
|
|
|
|
int remaining = static_cast<int>(
|
|
|
|
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
|
deadline - now).count());
|
|
|
|
|
|
if (remaining < 0) remaining = 0;
|
|
|
|
|
|
|
|
|
|
|
|
pollfd pfds[2] = {
|
|
|
|
|
|
{ fds[0], POLLIN, 0 },
|
|
|
|
|
|
{ wlFd, POLLIN, 0 },
|
|
|
|
|
|
};
|
|
|
|
|
|
const int n = ::poll(pfds, 2, remaining);
|
|
|
|
|
|
if (n < 0) {
|
|
|
|
|
|
wl_display_cancel_read(Device::display);
|
|
|
|
|
|
if (errno == EINTR) continue;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (n == 0) {
|
|
|
|
|
|
wl_display_cancel_read(Device::display);
|
|
|
|
|
|
break; // timeout
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pfds[1].revents & POLLIN) {
|
|
|
|
|
|
wl_display_read_events(Device::display);
|
|
|
|
|
|
wl_display_dispatch_pending(Device::display);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
wl_display_cancel_read(Device::display);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pfds[0].revents & (POLLIN | POLLHUP)) {
|
|
|
|
|
|
char buf[4096];
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
const ssize_t r = ::read(fds[0], buf, sizeof(buf));
|
|
|
|
|
|
if (r > 0) {
|
|
|
|
|
|
out.append(buf, static_cast<std::size_t>(r));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (r == 0) { done = true; break; } // EOF
|
|
|
|
|
|
if (errno == EINTR) continue;
|
|
|
|
|
|
if (errno == EAGAIN) break; // shouldn't happen (blocking), but harmless
|
|
|
|
|
|
done = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pfds[0].revents & POLLERR) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
::close(fds[0]);
|
|
|
|
|
|
if (out.empty()) return std::nullopt;
|
|
|
|
|
|
return out;
|
|
|
|
|
|
#elif defined(CRAFTER_GRAPHICS_WINDOW_WIN32)
|
|
|
|
|
|
// OS owns the wide-char buffer until we CloseClipboard, so the
|
|
|
|
|
|
// copy-out into std::string must happen before we release the lock.
|
|
|
|
|
|
if (!OpenClipboard(nullptr)) return std::nullopt;
|
|
|
|
|
|
HANDLE h = GetClipboardData(CF_UNICODETEXT);
|
|
|
|
|
|
if (h == nullptr) { CloseClipboard(); return std::nullopt; }
|
|
|
|
|
|
const wchar_t* src = static_cast<const wchar_t*>(GlobalLock(h));
|
|
|
|
|
|
if (src == nullptr) { CloseClipboard(); return std::nullopt; }
|
|
|
|
|
|
|
|
|
|
|
|
// CF_UNICODETEXT is NUL-terminated; lstrlenW gives us the length
|
|
|
|
|
|
// without the terminator. WideCharToMultiByte with length excluding
|
|
|
|
|
|
// the terminator produces a length-prefixed UTF-8 buffer we can
|
|
|
|
|
|
// size std::string for exactly.
|
|
|
|
|
|
const int wlen = lstrlenW(src);
|
|
|
|
|
|
std::string out;
|
|
|
|
|
|
if (wlen > 0) {
|
|
|
|
|
|
const int u8len = WideCharToMultiByte(CP_UTF8, 0, src, wlen,
|
|
|
|
|
|
nullptr, 0, nullptr, nullptr);
|
|
|
|
|
|
if (u8len > 0) {
|
|
|
|
|
|
out.resize(static_cast<std::size_t>(u8len));
|
|
|
|
|
|
WideCharToMultiByte(CP_UTF8, 0, src, wlen,
|
|
|
|
|
|
out.data(), u8len, nullptr, nullptr);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
GlobalUnlock(h);
|
|
|
|
|
|
CloseClipboard();
|
|
|
|
|
|
if (out.empty()) return std::nullopt;
|
|
|
|
|
|
return out;
|
|
|
|
|
|
#else
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|