Files
LookingGlass/client/displayservers/X11/clipboard.c
Geoffrey McRae 4c53f5b9f8
Some checks are pending
build / client (Debug, map[cc:clang cxx:clang++], libdecor) (push) Waiting to run
build / client (Debug, map[cc:clang cxx:clang++], xdg-shell) (push) Waiting to run
build / client (Debug, map[cc:gcc cxx:g++], libdecor) (push) Waiting to run
build / client (Debug, map[cc:gcc cxx:g++], xdg-shell) (push) Waiting to run
build / client (Release, map[cc:clang cxx:clang++], libdecor) (push) Waiting to run
build / client (Release, map[cc:clang cxx:clang++], xdg-shell) (push) Waiting to run
build / client (Release, map[cc:gcc cxx:g++], libdecor) (push) Waiting to run
build / client (Release, map[cc:gcc cxx:g++], xdg-shell) (push) Waiting to run
build / module (push) Waiting to run
build / host-linux (push) Waiting to run
build / host-windows-cross (push) Waiting to run
build / host-windows-native (push) Waiting to run
build / idd (push) Waiting to run
build / obs (clang) (push) Waiting to run
build / obs (gcc) (push) Waiting to run
build / docs (push) Waiting to run
[client] clipboard: add transport provider interface
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.
2026-08-09 20:10:46 +10:00

681 lines
16 KiB
C

/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "clipboard.h"
#include "x11.h"
#include "atoms.h"
#include <string.h>
#include <unistd.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#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;
struct X11ClipboardRead read;
};
static const char * atomTypes[] =
{
"UTF8_STRING",
"image/png",
"image/bmp",
"image/tiff",
"image/jpeg"
};
static struct X11ClipboardState x11cb;
// forwards
static void x11CBSelectionRequest(const XSelectionRequestEvent e);
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)
{
switch(xe->type)
{
case SelectionRequest:
x11CBSelectionRequest(xe->xselectionrequest);
return true;
case SelectionClear:
x11CBSelectionClear(xe->xselectionclear);
return true;
case SelectionNotify:
x11CBSelectionNotify(xe->xselection);
return true;
case PropertyNotify:
if (xe->xproperty.state != PropertyNewValue)
break;
if (xe->xproperty.atom == x11atoms.SEL_DATA)
{
x11CBSelectionIncr(xe->xproperty);
return true;
}
break;
default:
if (xe->type == x11.eventBase + XFixesSelectionNotify)
{
XFixesSelectionNotifyEvent * sne = (XFixesSelectionNotifyEvent *)xe;
x11CBXFixesSelectionNotify(*sne);
return true;
}
break;
}
return false;
}
bool x11CBInit(void)
{
LG_LOCK_INIT(x11cb.lock);
x11cb.aCurSelection = BadValue;
for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
{
x11cb.aTypes[i] = XInternAtom(x11.display, atomTypes[i], False);
if (x11cb.aTypes[i] == BadAlloc || x11cb.aTypes[i] == BadValue)
{
DEBUG_ERROR("failed to get atom for type: %s", atomTypes[i]);
return false;
}
}
// use xfixes to get clipboard change notifications
if (!XFixesQueryExtension(x11.display, &x11.eventBase, &x11.errorBase))
{
DEBUG_ERROR("failed to initialize xfixes");
return false;
}
XFixesSelectSelectionInput(x11.display, x11.window,
x11atoms.CLIPBOARD, XFixesSetSelectionOwnerNotifyMask);
return true;
}
static void x11CBReplyFn(void * opaque, LG_ClipboardData type,
const uint8_t * data, uint32_t size)
{
XEvent *s = (XEvent *)opaque;
if (type == LG_CLIPBOARD_DATA_NONE)
s->xselection.property = None;
else
XChangeProperty(
x11.display ,
s->xselection.requestor,
s->xselection.property ,
s->xselection.target ,
8,
PropModeReplace,
data,
size);
XSendEvent(x11.display, s->xselection.requestor, 0, 0, s);
XFlush(x11.display);
free(s);
}
static void x11CBSelectionRequest(const XSelectionRequestEvent e)
{
XEvent * s = malloc(sizeof(*s));
if (!s)
{
DEBUG_ERROR("out of memory");
return;
}
s->xselection.type = SelectionNotify;
s->xselection.requestor = e.requestor;
s->xselection.selection = e.selection;
s->xselection.target = e.target;
s->xselection.property = e.property;
s->xselection.time = e.time;
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
if (e.target == x11atoms.TARGETS)
{
Atom targets[2];
targets[0] = x11atoms.TARGETS;
targets[1] = x11cb.aTypes[requestType];
XChangeProperty(
e.display,
e.requestor,
e.property,
XA_ATOM,
32,
PropModeReplace,
(unsigned char*)targets,
ARRAY_LENGTH(targets)
);
goto send;
}
// 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 && requestType == i)
{
// request the data
if (app_clipboardRequest(requestType, x11CBReplyFn, s))
return;
goto nodata;
}
nodata:
// report no data
s->xselection.property = None;
send:
XSendEvent(x11.display, e.requestor, 0, 0, s);
XFlush(x11.display);
free(s);
}
static void x11CBSelectionClear(const XSelectionClearEvent e)
{
(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;
XDestroyWindow(x11.display, x11cb.targetsWindow);
x11cb.targetsWindow = 0;
}
static void x11CBSelectionIncr(const XPropertyEvent e)
{
Atom type;
int format;
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,
e.window,
e.atom,
0, ~0L, // start and length
True, // delete the property
AnyPropertyType,
&type,
&format,
&itemCount,
&after,
&data) != Success)
{
DEBUG_ERROR("XGetWindowProperty Failed");
const struct X11ClipboardRead read = clearReadNL();
LG_UNLOCK(x11cb.lock);
free(read.buffer);
app_clipboardAbort(read.request);
return;
}
LG_ClipboardData dataType;
for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType)
if (x11cb.aTypes[dataType] == type)
break;
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)
{
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,
x11cb.targetsWindow,
CurrentTime);
XUngrabServer(x11.display);
XFlush(x11.display);
LG_UNLOCK(x11cb.lock);
if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID)
app_clipboardAbort(oldRequest);
}
static void x11CBSelectionNotify(const XSelectionEvent e)
{
Atom type;
int format;
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,
e.requestor,
e.property,
0, ~0L, // start and length
True , // delete the property
AnyPropertyType,
&type,
&format,
&itemCount,
&after,
&data) != Success)
{
const LG_ClipboardRequest request = cancelReadNL();
LG_UNLOCK(x11cb.lock);
app_clipboardAbort(request);
return;
}
if (type == x11atoms.INCR)
{
if (!data || format != 32 || itemCount < 1)
{
const LG_ClipboardRequest request = cancelReadNL();
LG_UNLOCK(x11cb.lock);
if (data)
XFree(data);
app_clipboardAbort(request);
return;
}
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)
{
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;
}
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);
}
void x11CBNotice(LG_ClipboardData 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;
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_ClipboardRequest request, LG_ClipboardData type)
{
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,
window,
CurrentTime);
XFlush(x11.display);
LG_UNLOCK(x11cb.lock);
if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID)
app_clipboardAbort(oldRequest);
}