[client] clipboard: add transport provider interface
Some checks failed
build / client (Debug, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / module (push) Has been cancelled
build / host-linux (push) Has been cancelled
build / host-windows-cross (push) Has been cancelled
build / host-windows-native (push) Has been cancelled
build / idd (push) Has been cancelled
build / obs (clang) (push) Has been cancelled
build / obs (gcc) (push) Has been cancelled
build / docs (push) Has been cancelled

Route clipboard exchange through a provider interface. Connected
transports take priority while SPICE remains the fallback.

Track asynchronous X11 and Wayland transfers by request, preserving
clipboard ownership across provider changes.

Use PureSpice clipboard status notifications for capability and
lifecycle updates.
This commit is contained in:
Geoffrey McRae
2026-08-09 20:10:46 +10:00
parent 0da4bfa8dd
commit 4c53f5b9f8
22 changed files with 2287 additions and 539 deletions

View File

@@ -191,6 +191,44 @@ static bool hasImageMimetype(char ** mimetypes)
return false;
}
/* wlCb.lock must be held. */
static struct ClipboardRead * clipboardReadTakeCurrentNL(void)
{
struct ClipboardRead * data = wlCb.currentRead;
if (!data)
return NULL;
wlCb.currentRead = NULL;
return data;
}
static void clipboardReadRetire(struct ClipboardRead * data)
{
if (data)
waylandPollUnregister(data->fd);
}
static bool clipboardReadClaim(struct ClipboardRead * data)
{
LG_LOCK(wlCb.lock);
const bool current = wlCb.currentRead == data;
if (current)
clipboardReadTakeCurrentNL();
LG_UNLOCK(wlCb.lock);
if (current)
clipboardReadRetire(data);
return current;
}
static void clipboardReadCleanup(void * opaque)
{
struct ClipboardRead * data = opaque;
close(data->fd);
free(data->buf);
free(data);
}
// Destination client handlers.
static void dataOfferHandleOffer(void * opaque, struct wl_data_offer * offer,
@@ -276,21 +314,36 @@ static void dataDeviceHandleSelection(void * opaque,
return;
}
wlCb.offer = offer;
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
free(wlCb.mimetypes[i]);
memcpy(wlCb.mimetypes, extra->mimetypes, sizeof(wlCb.mimetypes));
wl_data_offer_set_user_data(offer, NULL);
free(extra);
int idx = 0;
enum LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE];
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
if (wlCb.mimetypes[i])
if (extra->mimetypes[i])
types[idx++] = i;
char * oldMimetypes[LG_CLIPBOARD_DATA_NONE];
LG_LOCK(wlCb.lock);
struct ClipboardRead * read = clipboardReadTakeCurrentNL();
struct wl_data_offer * oldOffer = wlCb.offer;
memcpy(oldMimetypes, wlCb.mimetypes, sizeof(oldMimetypes));
wlCb.offer = offer;
memcpy(wlCb.mimetypes, extra->mimetypes, sizeof(wlCb.mimetypes));
LG_UNLOCK(wlCb.lock);
wl_data_offer_set_user_data(offer, NULL);
memset(extra->mimetypes, 0, sizeof(extra->mimetypes));
free(extra);
if (read)
{
const LG_ClipboardRequest request = read->request;
clipboardReadRetire(read);
app_clipboardAbort(request);
}
if (oldOffer && oldOffer != offer)
wl_data_offer_destroy(oldOffer);
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
free(oldMimetypes[i]);
app_clipboardNotifyTypes(types, idx);
}
@@ -340,8 +393,6 @@ static const struct wl_data_device_listener dataDeviceListener = {
bool waylandCBInit(void)
{
memset(&wlCb, 0, sizeof(wlCb));
if (!wlWm.seat)
{
DEBUG_WARN("Clipboard unavailable without wl_seat");
@@ -370,37 +421,37 @@ bool waylandCBInit(void)
return true;
}
static void clipboardReadCancel(struct ClipboardRead * data)
{
waylandPollUnregister(data->fd);
close(data->fd);
free(data->buf);
free(data);
wlCb.currentRead = NULL;
}
static void clipboardReadCallback(uint32_t events, void * opaque)
{
struct ClipboardRead * data = opaque;
LG_LOCK(wlCb.lock);
const bool active = wlCb.currentRead == data;
LG_UNLOCK(wlCb.lock);
if (!active)
return;
if (events & EPOLLERR)
{
clipboardReadCancel(data);
if (clipboardReadClaim(data))
app_clipboardAbort(data->request);
return;
}
ssize_t result = read(data->fd, data->buf + data->numRead, data->size - data->numRead);
ssize_t result = read(data->fd,
data->buf + data->numRead, data->size - data->numRead);
if (result < 0)
{
DEBUG_ERROR("Failed to read from clipboard: %s", strerror(errno));
clipboardReadCancel(data);
if (clipboardReadClaim(data))
app_clipboardAbort(data->request);
return;
}
if (result == 0)
{
app_clipboardNotifySize(data->type, data->numRead);
app_clipboardData(data->type, data->buf, data->numRead);
clipboardReadCancel(data);
if (clipboardReadClaim(data))
app_clipboardData(
data->request, data->type, data->buf, data->numRead);
return;
}
@@ -409,9 +460,11 @@ static void clipboardReadCallback(uint32_t events, void * opaque)
{
data->size *= 2;
void * nbuf = realloc(data->buf, data->size);
if (!nbuf) {
if (!nbuf)
{
DEBUG_ERROR("Failed to realloc clipboard buffer: %s", strerror(errno));
clipboardReadCancel(data);
if (clipboardReadClaim(data))
app_clipboardAbort(data->request);
return;
}
@@ -421,70 +474,103 @@ static void clipboardReadCallback(uint32_t events, void * opaque)
void waylandCBInvalidate(void)
{
if (wlCb.currentRead)
clipboardReadCancel(wlCb.currentRead);
char * mimetypes[LG_CLIPBOARD_DATA_NONE];
LG_LOCK(wlCb.lock);
struct ClipboardRead * read = clipboardReadTakeCurrentNL();
struct wl_data_offer * offer = wlCb.offer;
wlCb.offer = NULL;
memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes));
memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes));
LG_UNLOCK(wlCb.lock);
if (read)
{
const LG_ClipboardRequest request = read->request;
clipboardReadRetire(read);
app_clipboardAbort(request);
}
app_clipboardRelease();
if (wlCb.offer)
wl_data_offer_destroy(wlCb.offer);
wlCb.offer = NULL;
if (offer)
wl_data_offer_destroy(offer);
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
free(mimetypes[i]);
}
void waylandCBRequest(LG_ClipboardData type)
void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type)
{
if (!wlCb.offer || !wlCb.mimetypes[type])
{
app_clipboardRelease();
if (request == LG_CLIPBOARD_REQUEST_INVALID ||
type < LG_CLIPBOARD_DATA_TEXT || type >= LG_CLIPBOARD_DATA_NONE)
return;
}
if (wlCb.currentRead)
clipboardReadCancel(wlCb.currentRead);
int fds[2];
if (pipe(fds) < 0)
{
DEBUG_ERROR("Failed to get a clipboard pipe: %s", strerror(errno));
abort();
app_clipboardAbort(request);
return;
}
wl_data_offer_receive(wlCb.offer, wlCb.mimetypes[type], fds[1]);
close(fds[1]);
struct ClipboardRead * data = malloc(sizeof(*data));
if (!data)
{
DEBUG_ERROR("Failed to allocate memory to read clipboard");
close(fds[0]);
close(fds[1]);
app_clipboardAbort(request);
return;
}
data->fd = fds[0];
data->size = 4096;
data->numRead = 0;
data->buf = malloc(data->size);
data->offer = wlCb.offer;
data->type = type;
data->fd = fds[0];
data->size = 4096;
data->numRead = 0;
data->buf = malloc(data->size);
data->request = request;
data->type = type;
if (!data->buf)
{
DEBUG_ERROR("Failed to allocate memory to receive clipboard data");
close(data->fd);
close(fds[1]);
free(data);
app_clipboardAbort(request);
return;
}
if (!waylandPollRegister(data->fd, clipboardReadCallback, data, EPOLLIN))
LG_LOCK(wlCb.lock);
struct ClipboardRead * old = clipboardReadTakeCurrentNL();
const bool available = wlCb.offer && wlCb.mimetypes[type];
if (available)
{
DEBUG_ERROR("Failed to register clipboard read into epoll: %s", strerror(errno));
close(data->fd);
free(data->buf);
free(data);
wl_data_offer_receive(
wlCb.offer, wlCb.mimetypes[type], fds[1]);
}
close(fds[1]);
const bool registered = available &&
waylandPollRegisterWithCleanup(data->fd,
clipboardReadCallback, data, clipboardReadCleanup, EPOLLIN);
if (registered)
wlCb.currentRead = data;
LG_UNLOCK(wlCb.lock);
if (old)
{
const LG_ClipboardRequest oldRequest = old->request;
clipboardReadRetire(old);
app_clipboardAbort(oldRequest);
}
if (!registered)
{
if (available)
DEBUG_ERROR("Failed to register clipboard read into epoll: %s",
strerror(errno));
clipboardReadCleanup(data);
app_clipboardAbort(request);
return;
}
wlCb.currentRead = data;
}
struct ClipboardWrite
@@ -494,6 +580,14 @@ struct ClipboardWrite
struct CountedBuffer * buffer;
};
static void clipboardWriteCleanup(void * opaque)
{
struct ClipboardWrite * data = opaque;
close(data->fd);
countedBufferRelease(&data->buffer);
free(data);
}
static void clipboardWriteCallback(uint32_t events, void * opaque)
{
struct ClipboardWrite * data = opaque;
@@ -514,9 +608,6 @@ static void clipboardWriteCallback(uint32_t events, void * opaque)
error:
waylandPollUnregister(data->fd);
close(data->fd);
countedBufferRelease(&data->buffer);
free(data);
}
static void dataSourceHandleTarget(void * data, struct wl_data_source * source,
@@ -543,8 +634,12 @@ static void dataSourceHandleSend(void * data, struct wl_data_source * source,
data->pos = 0;
data->buffer = transfer->data;
countedBufferAddRef(transfer->data);
waylandPollRegister(fd, clipboardWriteCallback, data, EPOLLOUT);
return;
if (waylandPollRegisterWithCleanup(fd,
clipboardWriteCallback, data, clipboardWriteCleanup, EPOLLOUT))
return;
countedBufferRelease(&data->buffer);
free(data);
}
error:
@@ -567,7 +662,7 @@ static const struct wl_data_source_listener dataSourceListener = {
};
static void waylandCBReplyFn(void * opaque, LG_ClipboardData type,
uint8_t * data, uint32_t size)
const uint8_t * data, uint32_t size)
{
if (type == LG_CLIPBOARD_DATA_NONE)
return;
@@ -587,7 +682,8 @@ static void waylandCBReplyFn(void * opaque, LG_ClipboardData type,
free(transfer);
return;
}
memcpy(transfer->data->data, data, size);
if (size)
memcpy(transfer->data->data, data, size);
struct wl_data_source * source =
wl_data_device_manager_create_data_source(wlWm.dataDeviceManager);
@@ -602,13 +698,38 @@ static void waylandCBReplyFn(void * opaque, LG_ClipboardData type,
void waylandCBNotice(LG_ClipboardData type)
{
wlCb.haveRequest = true;
wlCb.type = type;
if (!app_clipboardRequest(waylandCBReplyFn, NULL))
DEBUG_ERROR("Failed to request SPICE clipboard data");
if (!app_clipboardRequest(type, waylandCBReplyFn, NULL))
DEBUG_ERROR("Failed to request remote clipboard data");
}
void waylandCBRelease(void)
{
wlCb.haveRequest = false;
}
void waylandCBFree(void)
{
char * mimetypes[LG_CLIPBOARD_DATA_NONE];
LG_LOCK(wlCb.lock);
struct ClipboardRead * read = clipboardReadTakeCurrentNL();
struct wl_data_offer * offer = wlCb.offer;
struct wl_data_offer * dndOffer = wlCb.dndOffer;
struct wl_data_device * dataDevice = wlCb.dataDevice;
wlCb.offer = NULL;
wlCb.dndOffer = NULL;
wlCb.dataDevice = NULL;
memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes));
memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes));
LG_UNLOCK(wlCb.lock);
clipboardReadRetire(read);
if (offer)
wl_data_offer_destroy(offer);
if (dndOffer && dndOffer != offer)
wl_data_offer_destroy(dndOffer);
if (dataDevice)
wl_data_device_release(dataDevice);
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
free(mimetypes[i]);
LG_LOCK_FREE(wlCb.lock);
}

View File

@@ -263,7 +263,7 @@ void libdecor_pollWait(struct wl_display * display, int epollFd,
for (int i = 0; i < count; ++i)
{
struct WaylandPoll * poll = events[i].data.ptr;
if (!poll->removed)
if (!atomic_load_explicit(&poll->removed, memory_order_acquire))
poll->callback(events[i].events, poll->opaque);
}
}

View File

@@ -300,7 +300,7 @@ void xdg_pollWait(struct wl_display * display, int epollFd,
bool sawDisplay = false;
for (int i = 0; i < count; ++i) {
struct WaylandPoll * poll = events[i].data.ptr;
if (!poll->removed)
if (!atomic_load_explicit(&poll->removed, memory_order_acquire))
poll->callback(events[i].events, poll->opaque);
if (poll->fd == state.displayFd)
sawDisplay = true;

View File

@@ -25,6 +25,7 @@
#include <errno.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <wayland-client.h>
#include "common/debug.h"
@@ -47,84 +48,150 @@ bool waylandPollInit(void)
return wlWm.desktop->pollInit(wlWm.display);
}
void waylandWait(unsigned int time)
static void waylandPollDestroyNode(struct WaylandPoll * node)
{
wlWm.desktop->pollWait(wlWm.display, wlWm.epollFd, time);
INTERLOCKED_SECTION(wlWm.pollFreeLock,
{
struct WaylandPoll * node;
struct WaylandPoll * temp;
wl_list_for_each_safe(node, temp, &wlWm.pollFree, link)
{
wl_list_remove(&node->link);
free(node);
}
});
if (node->cleanup)
node->cleanup(node->opaque);
free(node);
}
static void waylandPollRemoveNode(struct WaylandPoll * node)
static void waylandPollDestroyList(struct wl_list * list)
{
INTERLOCKED_SECTION(wlWm.pollLock,
struct WaylandPoll * node;
struct WaylandPoll * temp;
wl_list_for_each_safe(node, temp, list, link)
{
wl_list_remove(&node->link);
});
waylandPollDestroyNode(node);
}
}
bool waylandPollRegister(int fd, WaylandPollCallback callback, void * opaque, uint32_t events)
static void waylandPollTakeList(struct wl_list * source,
struct wl_list * destination)
{
struct WaylandPoll * node;
struct WaylandPoll * temp;
wl_list_for_each_safe(node, temp, source, link)
{
wl_list_remove(&node->link);
wl_list_insert(destination, &node->link);
}
}
void waylandWait(unsigned int time)
{
INTERLOCKED_SECTION(wlWm.pollFreeLock,
{
++wlWm.pollWaiters;
});
wlWm.desktop->pollWait(wlWm.display, wlWm.epollFd, time);
struct wl_list retired;
wl_list_init(&retired);
INTERLOCKED_SECTION(wlWm.pollFreeLock,
{
if (--wlWm.pollWaiters == 0)
waylandPollTakeList(&wlWm.pollFree, &retired);
});
waylandPollDestroyList(&retired);
}
void waylandPollFree(void)
{
if (wlWm.epollFd >= 0)
{
close(wlWm.epollFd);
wlWm.epollFd = -1;
}
struct wl_list active;
struct wl_list retired;
wl_list_init(&active);
wl_list_init(&retired);
INTERLOCKED_SECTION(wlWm.pollLock,
{
waylandPollTakeList(&wlWm.poll, &active);
});
INTERLOCKED_SECTION(wlWm.pollFreeLock,
{
waylandPollTakeList(&wlWm.pollFree, &retired);
});
waylandPollDestroyList(&active);
waylandPollDestroyList(&retired);
LG_LOCK_FREE(wlWm.pollFreeLock);
LG_LOCK_FREE(wlWm.pollLock);
}
bool waylandPollRegisterWithCleanup(int fd, WaylandPollCallback callback,
void * opaque, WaylandPollCleanup cleanup, uint32_t events)
{
struct WaylandPoll * node = malloc(sizeof(*node));
if (!node)
return false;
node->fd = fd;
node->removed = false;
node->callback = callback;
node->cleanup = cleanup;
node->opaque = opaque;
atomic_init(&node->removed, false);
INTERLOCKED_SECTION(wlWm.pollLock,
{
wl_list_insert(&wlWm.poll, &node->link);
});
LG_LOCK(wlWm.pollLock);
if (epoll_ctl(wlWm.epollFd, EPOLL_CTL_ADD, fd, &(struct epoll_event) {
.events = events,
.data = (epoll_data_t) { .ptr = node },
}) < 0)
{
waylandPollRemoveNode(node);
LG_UNLOCK(wlWm.pollLock);
free(node);
return false;
}
wl_list_insert(&wlWm.poll, &node->link);
LG_UNLOCK(wlWm.pollLock);
return true;
}
bool waylandPollRegister(int fd, WaylandPollCallback callback,
void * opaque, uint32_t events)
{
return waylandPollRegisterWithCleanup(
fd, callback, opaque, NULL, events);
}
bool waylandPollUnregister(int fd)
{
struct WaylandPoll * node = NULL;
INTERLOCKED_SECTION(wlWm.pollLock,
LG_LOCK(wlWm.pollLock);
struct WaylandPoll * current;
wl_list_for_each(current, &wlWm.poll, link)
{
wl_list_for_each(node, &wlWm.poll, link)
if (current->fd == fd)
{
if (node->fd == fd)
break;
node = current;
atomic_store_explicit(
&node->removed, true, memory_order_release);
break;
}
});
}
if (!node)
{
LG_UNLOCK(wlWm.pollLock);
DEBUG_ERROR("Attempt to unregister a fd that was not registered: %d", fd);
return false;
}
node->removed = true;
if (epoll_ctl(wlWm.epollFd, EPOLL_CTL_DEL, fd, NULL) < 0)
{
DEBUG_ERROR("Failed to unregistered from epoll: %s", strerror(errno));
LG_UNLOCK(wlWm.pollLock);
DEBUG_ERROR("Failed to unregister from epoll: %s", strerror(errno));
return false;
}
waylandPollRemoveNode(node);
wl_list_remove(&node->link);
LG_UNLOCK(wlWm.pollLock);
INTERLOCKED_SECTION(wlWm.pollFreeLock,
{

View File

@@ -108,9 +108,11 @@ static bool getCompositor(char * dst, size_t size)
static bool waylandInit(const LG_DSInitParams params)
{
memset(&wlWm, 0, sizeof(wlWm));
memset(&wlCb, 0, sizeof(wlCb));
LG_LOCK_INIT(wlWm.surfaceLock);
LG_LOCK_INIT(wlWm.pendingHDRLock);
LG_LOCK_INIT(wlWm.hdrLock);
LG_LOCK_INIT(wlCb.lock);
wlWm.desktop = WL_Desktops[0];
atomic_init(&wlWm.lockActive, false);
atomic_init(&wlWm.cmFeaturesDone, false);
@@ -207,6 +209,7 @@ static void waylandShutdown(void)
static void waylandFree(void)
{
waylandCBFree();
waylandIdleFree();
waylandPresentationFree();
waylandWindowFree();
@@ -215,6 +218,7 @@ static void waylandFree(void)
waylandColorMgmtFree();
waylandRegistryFree();
waylandCursorFree();
waylandPollFree();
wl_display_disconnect(wlWm.display);
LG_LOCK_FREE(wlWm.hdrLock);
LG_LOCK_FREE(wlWm.pendingHDRLock);

View File

@@ -59,14 +59,16 @@
#include "motion.h"
typedef void (*WaylandPollCallback)(uint32_t events, void * opaque);
typedef void (*WaylandPollCleanup)(void * opaque);
struct WaylandPoll
{
int fd;
bool removed;
int fd;
atomic_bool removed;
WaylandPollCallback callback;
void * opaque;
struct wl_list link;
WaylandPollCleanup cleanup;
void * opaque;
struct wl_list link;
};
struct OutputColorDescription;
@@ -273,40 +275,39 @@ struct WaylandDSState
struct wl_list poll; // WaylandPoll::link
struct wl_list pollFree; // WaylandPoll::link
LG_Lock pollLock;
LG_Lock pollFreeLock;
int epollFd;
int displayFd;
LG_Lock pollLock;
LG_Lock pollFreeLock;
unsigned int pollWaiters;
int epollFd;
int displayFd;
};
struct WCBTransfer
{
struct CountedBuffer * data;
const char ** mimetypes;
const char ** mimetypes;
};
struct ClipboardRead
{
int fd;
size_t size;
size_t numRead;
uint8_t * buf;
enum LG_ClipboardData type;
struct wl_data_offer * offer;
int fd;
size_t size;
size_t numRead;
uint8_t * buf;
LG_ClipboardRequest request;
LG_ClipboardData type;
};
struct WCBState
{
struct wl_data_device * dataDevice;
char lgMimetype[64];
char lgMimetype[64];
char * mimetypes[LG_CLIPBOARD_DATA_NONE];
char * mimetypes[LG_CLIPBOARD_DATA_NONE];
struct wl_data_offer * offer;
struct wl_data_offer * dndOffer;
bool haveRequest;
LG_ClipboardData type;
LG_Lock lock;
struct ClipboardRead * currentRead;
};
@@ -320,7 +321,8 @@ void waylandActivationRequestActivation(void);
// clipboard module
bool waylandCBInit(void);
void waylandCBRequest(LG_ClipboardData type);
void waylandCBFree(void);
void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type);
void waylandCBNotice(LG_ClipboardData type);
void waylandCBRelease(void);
void waylandCBInvalidate(void);
@@ -407,8 +409,11 @@ void waylandOutputUpdateHDRWhiteLevel(void);
// poll module
bool waylandPollInit(void);
void waylandPollFree(void);
void waylandWait(unsigned int time);
bool waylandPollRegister(int fd, WaylandPollCallback callback, void * opaque, uint32_t events);
bool waylandPollRegisterWithCleanup(int fd, WaylandPollCallback callback,
void * opaque, WaylandPollCleanup cleanup, uint32_t events);
bool waylandPollUnregister(int fd);
// presentation module

View File

@@ -31,16 +31,29 @@
#include "app.h"
#include "common/array.h"
#include "common/debug.h"
#include "common/locking.h"
struct X11ClipboardRead
{
Window window;
LG_ClipboardRequest request;
LG_ClipboardData type;
bool incremental;
uint8_t * buffer;
size_t size;
size_t capacity;
};
struct X11ClipboardState
{
LG_Lock lock;
Atom aCurSelection;
Atom aTypes[LG_CLIPBOARD_DATA_NONE];
Window targetsWindow;
LG_ClipboardData type;
bool haveRequest;
bool incrStart;
unsigned int lowerBound;
struct X11ClipboardRead read;
};
static const char * atomTypes[] =
@@ -60,6 +73,8 @@ static void x11CBSelectionClear(const XSelectionClearEvent e);
static void x11CBSelectionIncr(const XPropertyEvent e);
static void x11CBSelectionNotify(const XSelectionEvent e);
static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e);
static LG_ClipboardRequest cancelReadNL(void);
static void clearTargetsNL(void);
bool x11CBEventThread(const XEvent * xe)
{
@@ -83,9 +98,6 @@ bool x11CBEventThread(const XEvent * xe)
if (xe->xproperty.atom == x11atoms.SEL_DATA)
{
if (x11cb.lowerBound == 0)
return true;
x11CBSelectionIncr(xe->xproperty);
return true;
}
@@ -106,6 +118,7 @@ bool x11CBEventThread(const XEvent * xe)
bool x11CBInit(void)
{
LG_LOCK_INIT(x11cb.lock);
x11cb.aCurSelection = BadValue;
for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
{
@@ -131,7 +144,7 @@ bool x11CBInit(void)
}
static void x11CBReplyFn(void * opaque, LG_ClipboardData type,
uint8_t * data, uint32_t size)
const uint8_t * data, uint32_t size)
{
XEvent *s = (XEvent *)opaque;
@@ -169,7 +182,12 @@ static void x11CBSelectionRequest(const XSelectionRequestEvent e)
s->xselection.property = e.property;
s->xselection.time = e.time;
if (!x11cb.haveRequest)
LG_LOCK(x11cb.lock);
const bool haveRequest = x11cb.haveRequest;
const LG_ClipboardData requestType = x11cb.type;
LG_UNLOCK(x11cb.lock);
if (!haveRequest)
goto nodata;
// target list requested
@@ -177,7 +195,7 @@ static void x11CBSelectionRequest(const XSelectionRequestEvent e)
{
Atom targets[2];
targets[0] = x11atoms.TARGETS;
targets[1] = x11cb.aTypes[x11cb.type];
targets[1] = x11cb.aTypes[requestType];
XChangeProperty(
e.display,
@@ -195,10 +213,10 @@ static void x11CBSelectionRequest(const XSelectionRequestEvent e)
// look to see if we can satisfy the data type
for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
if (x11cb.aTypes[i] == e.target && x11cb.type == i)
if (x11cb.aTypes[i] == e.target && requestType == i)
{
// request the data
if (app_clipboardRequest(x11CBReplyFn, s))
if (app_clipboardRequest(requestType, x11CBReplyFn, s))
return;
goto nodata;
}
@@ -215,20 +233,53 @@ send:
static void x11CBSelectionClear(const XSelectionClearEvent e)
{
if (e.selection != x11atoms.CLIPBOARD)
(void)e;
}
/* x11cb.lock must be held. */
static struct X11ClipboardRead clearReadNL(void)
{
const struct X11ClipboardRead read = x11cb.read;
x11cb.read = (struct X11ClipboardRead) { 0 };
if (read.window)
XDestroyWindow(x11.display, read.window);
return read;
}
/* x11cb.lock must be held. */
static LG_ClipboardRequest cancelReadNL(void)
{
const struct X11ClipboardRead read = clearReadNL();
free(read.buffer);
return read.request;
}
/* x11cb.lock must be held. */
static void clearTargetsNL(void)
{
if (!x11cb.targetsWindow)
return;
x11cb.aCurSelection = BadValue;
app_clipboardRelease();
return;
XDestroyWindow(x11.display, x11cb.targetsWindow);
x11cb.targetsWindow = 0;
}
static void x11CBSelectionIncr(const XPropertyEvent e)
{
Atom type;
int format;
unsigned long itemCount, after;
unsigned char *data;
unsigned long itemCount;
unsigned long after;
unsigned char * data = NULL;
LG_LOCK(x11cb.lock);
if (!x11cb.read.window || !x11cb.read.incremental ||
e.window != x11cb.read.window ||
e.atom != x11atoms.SEL_DATA)
{
LG_UNLOCK(x11cb.lock);
return;
}
if (XGetWindowProperty(
e.display,
@@ -236,49 +287,7 @@ static void x11CBSelectionIncr(const XPropertyEvent e)
e.atom,
0, ~0L, // start and length
True, // delete the property
x11atoms.INCR,
&type,
&format,
&itemCount,
&after,
&data) != Success)
{
DEBUG_INFO("GetProp Failed");
app_clipboardNotifySize(LG_CLIPBOARD_DATA_NONE, 0);
goto out;
}
LG_ClipboardData dataType;
for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType)
if (x11cb.aTypes[dataType] == type)
break;
if (dataType == LG_CLIPBOARD_DATA_NONE)
{
DEBUG_WARN("clipboard data (%s) not in a supported format",
XGetAtomName(x11.display, type));
x11cb.lowerBound = 0;
app_clipboardNotifySize(LG_CLIPBOARD_DATA_NONE, 0);
goto out;
}
if (x11cb.incrStart)
{
app_clipboardNotifySize(dataType, x11cb.lowerBound);
x11cb.incrStart = false;
}
XFree(data);
data = NULL;
if (XGetWindowProperty(
e.display,
e.window,
e.atom,
0, ~0L, // start and length
True, // delete the property
type,
AnyPropertyType,
&type,
&format,
&itemCount,
@@ -286,49 +295,233 @@ static void x11CBSelectionIncr(const XPropertyEvent e)
&data) != Success)
{
DEBUG_ERROR("XGetWindowProperty Failed");
app_clipboardNotifySize(LG_CLIPBOARD_DATA_NONE, 0);
goto out;
const struct X11ClipboardRead read = clearReadNL();
LG_UNLOCK(x11cb.lock);
free(read.buffer);
app_clipboardAbort(read.request);
return;
}
app_clipboardData(dataType, data, itemCount);
x11cb.lowerBound -= itemCount;
LG_ClipboardData dataType;
for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType)
if (x11cb.aTypes[dataType] == type)
break;
out:
if ((itemCount && !data) || dataType == LG_CLIPBOARD_DATA_NONE ||
dataType != x11cb.read.type || format != 8)
{
DEBUG_WARN("Invalid incremental clipboard data");
const struct X11ClipboardRead read = clearReadNL();
LG_UNLOCK(x11cb.lock);
if (data)
XFree(data);
free(read.buffer);
app_clipboardAbort(read.request);
return;
}
if (itemCount == 0)
{
const struct X11ClipboardRead read = clearReadNL();
LG_UNLOCK(x11cb.lock);
if (data)
XFree(data);
app_clipboardData(
read.request, read.type, read.buffer, read.size);
free(read.buffer);
return;
}
if (itemCount > SIZE_MAX - x11cb.read.size)
{
const struct X11ClipboardRead read = clearReadNL();
LG_UNLOCK(x11cb.lock);
XFree(data);
free(read.buffer);
app_clipboardAbort(read.request);
return;
}
const size_t required = x11cb.read.size + itemCount;
if (required > x11cb.read.capacity)
{
size_t capacity = x11cb.read.capacity;
while (capacity < required)
{
if (capacity > SIZE_MAX / 2)
{
capacity = required;
break;
}
capacity *= 2;
}
uint8_t * buffer = realloc(x11cb.read.buffer, capacity);
if (!buffer)
{
const struct X11ClipboardRead read = clearReadNL();
LG_UNLOCK(x11cb.lock);
XFree(data);
free(read.buffer);
app_clipboardAbort(read.request);
return;
}
x11cb.read.buffer = buffer;
x11cb.read.capacity = capacity;
}
memcpy(x11cb.read.buffer + x11cb.read.size, data, itemCount);
x11cb.read.size += itemCount;
LG_UNLOCK(x11cb.lock);
if (data)
XFree(data);
}
static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e)
{
// check if the selection is valid and it isn't ourself
if (e.selection != x11atoms.CLIPBOARD ||
e.owner == x11.window || e.owner == 0)
if (e.selection != x11atoms.CLIPBOARD || e.owner == x11.window)
return;
LG_LOCK(x11cb.lock);
XGrabServer(x11.display);
if (XGetSelectionOwner(x11.display, x11atoms.CLIPBOARD) != e.owner)
{
XUngrabServer(x11.display);
XFlush(x11.display);
LG_UNLOCK(x11cb.lock);
return;
}
const LG_ClipboardRequest oldRequest = x11cb.read.window ?
cancelReadNL() : LG_CLIPBOARD_REQUEST_INVALID;
clearTargetsNL();
if (e.owner == 0)
{
x11cb.aCurSelection = BadValue;
XUngrabServer(x11.display);
XFlush(x11.display);
LG_UNLOCK(x11cb.lock);
if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID)
app_clipboardAbort(oldRequest);
app_clipboardRelease();
return;
}
// remember which selection we are working with
x11cb.aCurSelection = e.selection;
x11cb.targetsWindow = XCreateSimpleWindow(
x11.display, x11.window, 0, 0, 1, 1, 0, 0, 0);
if (!x11cb.targetsWindow)
{
x11cb.aCurSelection = BadValue;
XUngrabServer(x11.display);
XFlush(x11.display);
LG_UNLOCK(x11cb.lock);
if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID)
app_clipboardAbort(oldRequest);
app_clipboardRelease();
return;
}
XConvertSelection(
x11.display,
e.selection,
x11atoms.TARGETS,
x11atoms.TARGETS,
x11.window,
x11cb.targetsWindow,
CurrentTime);
XUngrabServer(x11.display);
XFlush(x11.display);
LG_UNLOCK(x11cb.lock);
return;
if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID)
app_clipboardAbort(oldRequest);
}
static void x11CBSelectionNotify(const XSelectionEvent e)
{
if (e.property == None)
return;
Atom type;
int format;
unsigned long itemCount, after;
unsigned char *data;
unsigned long itemCount;
unsigned long after;
unsigned char * data = NULL;
LG_LOCK(x11cb.lock);
const bool targetReply = x11cb.targetsWindow &&
e.requestor == x11cb.targetsWindow && e.target == x11atoms.TARGETS;
if (targetReply)
{
if (e.property == None)
{
clearTargetsNL();
LG_UNLOCK(x11cb.lock);
app_clipboardRelease();
return;
}
if (XGetWindowProperty(
e.display,
e.requestor,
e.property,
0, ~0L, // start and length
True, // delete the property
AnyPropertyType,
&type,
&format,
&itemCount,
&after,
&data) != Success)
{
clearTargetsNL();
LG_UNLOCK(x11cb.lock);
app_clipboardRelease();
return;
}
clearTargetsNL();
LG_UNLOCK(x11cb.lock);
// the format is 32-bit and we must have data
// this is technically incorrect however as it's
// an array of padded 64-bit values
if (!data || format != 32)
{
app_clipboardRelease();
goto out;
}
int typeCount = 0;
LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE];
// see if we support any of the targets listed
const Atom * targets = (const Atom *)data;
for(int n = 0; n < LG_CLIPBOARD_DATA_NONE; ++n)
for(unsigned long i = 0; i < itemCount; ++i)
if (x11cb.aTypes[n] == targets[i])
{
types[typeCount++] = n;
break;
}
app_clipboardNotifyTypes(types, typeCount);
goto out;
}
LG_UNLOCK(x11cb.lock);
LG_LOCK(x11cb.lock);
if (!x11cb.read.window || e.requestor != x11cb.read.window)
{
LG_UNLOCK(x11cb.lock);
return;
}
if (e.property == None)
{
const LG_ClipboardRequest request = cancelReadNL();
LG_UNLOCK(x11cb.lock);
app_clipboardAbort(request);
return;
}
if (XGetWindowProperty(
e.display,
@@ -343,60 +536,65 @@ static void x11CBSelectionNotify(const XSelectionEvent e)
&after,
&data) != Success)
{
app_clipboardNotifySize(LG_CLIPBOARD_DATA_NONE, 0);
goto out;
const LG_ClipboardRequest request = cancelReadNL();
LG_UNLOCK(x11cb.lock);
app_clipboardAbort(request);
return;
}
if (type == x11atoms.INCR)
{
x11cb.incrStart = true;
x11cb.lowerBound = *(unsigned int *)data;
goto out;
}
// the target list
if (e.property == x11atoms.TARGETS)
{
// the format is 32-bit and we must have data
// this is technically incorrect however as it's
// an array of padded 64-bit values
if (!data || format != 32)
goto out;
int typeCount = 0;
LG_ClipboardData types[itemCount];
// see if we support any of the targets listed
const uint64_t * targets = (const uint64_t *)data;
for(unsigned long i = 0; i < itemCount; ++i)
if (!data || format != 32 || itemCount < 1)
{
for(int n = 0; n < LG_CLIPBOARD_DATA_NONE; ++n)
if (x11cb.aTypes[n] == targets[i])
types[typeCount++] = n;
const LG_ClipboardRequest request = cancelReadNL();
LG_UNLOCK(x11cb.lock);
if (data)
XFree(data);
app_clipboardAbort(request);
return;
}
app_clipboardNotifyTypes(types, typeCount);
goto out;
}
if (e.property == x11atoms.SEL_DATA)
{
LG_ClipboardData dataType;
for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType)
if (x11cb.aTypes[dataType] == type)
break;
if (dataType == LG_CLIPBOARD_DATA_NONE)
size_t capacity = *(const unsigned long *)data;
if (capacity < 4096)
capacity = 4096;
if (capacity > 1048576)
capacity = 1048576;
x11cb.read.buffer = malloc(capacity);
if (!x11cb.read.buffer)
{
DEBUG_WARN("clipboard data (%s) not in a supported format",
XGetAtomName(x11.display, type));
goto out;
const LG_ClipboardRequest request = cancelReadNL();
LG_UNLOCK(x11cb.lock);
XFree(data);
app_clipboardAbort(request);
return;
}
x11cb.read.capacity = capacity;
x11cb.read.incremental = true;
LG_UNLOCK(x11cb.lock);
XFree(data);
return;
}
app_clipboardData(dataType, data, itemCount);
LG_ClipboardData dataType;
for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType)
if (x11cb.aTypes[dataType] == type)
break;
const LG_ClipboardRequest request = x11cb.read.request;
const bool valid = dataType != LG_CLIPBOARD_DATA_NONE &&
dataType == x11cb.read.type && format == 8;
cancelReadNL();
LG_UNLOCK(x11cb.lock);
if (!valid)
{
DEBUG_WARN("Invalid clipboard data");
app_clipboardAbort(request);
goto out;
}
app_clipboardData(request, dataType, data, itemCount);
out:
if (data)
XFree(data);
@@ -404,29 +602,79 @@ out:
void x11CBNotice(LG_ClipboardData type)
{
x11cb.haveRequest = true;
x11cb.type = type;
LG_LOCK(x11cb.lock);
const LG_ClipboardRequest oldRequest = x11cb.read.window ?
cancelReadNL() : LG_CLIPBOARD_REQUEST_INVALID;
clearTargetsNL();
x11cb.aCurSelection = BadValue;
x11cb.haveRequest = true;
x11cb.type = type;
XSetSelectionOwner(x11.display, x11atoms.CLIPBOARD, x11.window, CurrentTime);
XFlush(x11.display);
LG_UNLOCK(x11cb.lock);
if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID)
app_clipboardAbort(oldRequest);
}
void x11CBRelease(void)
{
LG_LOCK(x11cb.lock);
x11cb.haveRequest = false;
XSetSelectionOwner(x11.display, x11atoms.CLIPBOARD, None, CurrentTime);
XGrabServer(x11.display);
if (XGetSelectionOwner(x11.display, x11atoms.CLIPBOARD) == x11.window)
XSetSelectionOwner(
x11.display, x11atoms.CLIPBOARD, None, CurrentTime);
XUngrabServer(x11.display);
XFlush(x11.display);
LG_UNLOCK(x11cb.lock);
}
void x11CBRequest(LG_ClipboardData type)
void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type)
{
if (x11cb.aCurSelection == BadValue)
if (request == LG_CLIPBOARD_REQUEST_INVALID ||
type < LG_CLIPBOARD_DATA_TEXT || type >= LG_CLIPBOARD_DATA_NONE)
return;
LG_LOCK(x11cb.lock);
const LG_ClipboardRequest oldRequest = x11cb.read.window ?
cancelReadNL() : LG_CLIPBOARD_REQUEST_INVALID;
if (x11cb.aCurSelection == BadValue)
{
LG_UNLOCK(x11cb.lock);
if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID)
app_clipboardAbort(oldRequest);
app_clipboardAbort(request);
return;
}
const Window window = XCreateSimpleWindow(
x11.display, x11.window, 0, 0, 1, 1, 0, 0, 0);
if (!window)
{
LG_UNLOCK(x11cb.lock);
if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID)
app_clipboardAbort(oldRequest);
app_clipboardAbort(request);
return;
}
XSelectInput(x11.display, window, PropertyChangeMask);
x11cb.read = (struct X11ClipboardRead)
{
.window = window,
.request = request,
.type = type,
};
XConvertSelection(
x11.display,
x11cb.aCurSelection,
x11cb.aTypes[type],
x11atoms.SEL_DATA,
x11.window,
window,
CurrentTime);
XFlush(x11.display);
LG_UNLOCK(x11cb.lock);
if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID)
app_clipboardAbort(oldRequest);
}

View File

@@ -31,6 +31,6 @@ bool x11CBEventThread(const XEvent * xe);
bool x11CBInit(void);
void x11CBNotice(LG_ClipboardData type);
void x11CBRelease(void);
void x11CBRequest(LG_ClipboardData type);
void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type);
#endif