X11 uses a flexible multi-buffer multi-format asynchronous application-side clipboard protocol.
Most toolkits have it implemented (GTK's gtk_clipboard_get()
, Qt's QApplication::clipboard()
, Tk's clipboard_get). But you can do it manually with X11 API, for example, if you're not using toolkits, or if you must pass large amount of data through clipboard buffer without keeping it all in memory at the same time.
Theory
There may be many buffers, but you only need to know about two:
CLIPBOARD
is the usual explicit buffer: you copy things there with Edit/Copy menu, and paste it with Edit/Paste menu.
PRIMARY
selection is an implicit mouse selection feature: text gets in it when selected with mouse cursor, and gets pasted from it on middle-click in text input fields.
Primary selection needs no keypresses, so it's useful for copying small fragments between windows that are next to each other. This feature is mostly unix-specific, but I've seen putty, trillian and some gtk apps emulating it on Windows OS. Also firefox has "Paste & Go" feature when middle-clicking empty non-interactive space of the page.
To optimize things those are application-side buffers: instead of pushing entire clipboard/selection to the server every time it changes, application just tells the server "I own it". To get the buffer you ask the owner to give you its content. This way even a large buffer takes no resources until its actually requested.
When requesting the buffer you ask the owner for a specific format you need. For example an image copied from seamonkey browser (right-click an image and press "Copy Image") can be represented in different formats. It would appear as image URL if you paste it in terminal. It would become a picture loaded from that URL if you paste it in libreoffice writer. And it'd be the image itself if pasted in gimp. That works because seamonkey is smart and provides each application with format it asks for: text string for terminal, html for libreoffice and image data for gimp. To request text format you'd ask for UTF8_STRING
format with fallback to STRING
.
As you ask another application to prepare the buffer, and that may take some time, the request is asynchronous: the owner prepares the buffer, saves it in a specified location (window property is used as a temporary storage) and notifies you with SelectionNotify
event when it's done.
So to get the buffer:
- choose buffer name (
CLIPBOARD
, PRIMARY
), format
(UTF8_STRING
, STRING
) and a window property to store result to
- call
XConvertSelection()
to request the buffer
- wait for
SelectionNotify
event
- read buffer content from window property
Naive implementation
// gcc -o xclipget xclipget.c -lX11
#include <stdio.h>
#include <limits.h>
#include <X11/Xlib.h>
Bool PrintSelection(Display *display, Window window, const char *bufname, const char *fmtname)
{
char *result;
unsigned long ressize, restail;
int resbits;
Atom bufid = XInternAtom(display, bufname, False),
fmtid = XInternAtom(display, fmtname, False),
propid = XInternAtom(display, "XSEL_DATA", False),
incrid = XInternAtom(display, "INCR", False);
XEvent event;
XConvertSelection(display, bufid, fmtid, propid, window, CurrentTime);
do {
XNextEvent(display, &event);
} while (event.type != SelectionNotify || event.xselection.selection != bufid);
if (event.xselection.property)
{
XGetWindowProperty(display, window, propid, 0, LONG_MAX/4, False, AnyPropertyType,
&fmtid, &resbits, &ressize, &restail, (unsigned char**)&result);
if (fmtid == incrid)
printf("Buffer is too large and INCR reading is not implemented yet.\n");
else
printf("%.*s", (int)ressize, result);
XFree(result);
return True;
}
else // request failed, e.g. owner can't convert to the target format
return False;
}
int main()
{
Display *display = XOpenDisplay(NULL);
unsigned long color = BlackPixel(display, DefaultScreen(display));
Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 0,0, 1,1, 0, color, color);
Bool result = PrintSelection(display, window, "CLIPBOARD", "UTF8_STRING") ||
PrintSelection(display, window, "CLIPBOARD", "STRING");
XDestroyWindow(display, window);
XCloseDisplay(display);
return !result;
}
This will work for many simple cases. One thing missing here is support for incremental reading of large buffers. Let's add it!
Large buffers
Some apps may want to copy/paste 100 gigabytes of text logs. And X11 allows that! But the data must be passed incrementally, split into chunks.
If the requested buffer is too large, instead of storing it into the window property, owner sets a property of format INCR
. If you delete it, owner assumes you've read it, and puts next chunk in the same property. That continues until the last chunk is read and deleted. Finally owner sets property of size 0 to mark the end of data.
So to read large buffer you delete INCR
property and wait for the property to appear again (PropertyNotify
event, state == PropertyNewValue
), read and delete it, wait for it to appear again, and so on until it appears with zero size.
// gcc -o xclipget xclipget.c -lX11
#include <stdio.h>
#include <limits.h>
#include <X11/Xlib.h>
Bool PrintSelection(Display *display, Window window, const char *bufname, const char *fmtname)
{
char *result;
unsigned long ressize, restail;
int resbits;
Atom bufid = XInternAtom(display, bufname, False),
fmtid = XInternAtom(display, fmtname, False),
propid = XInternAtom(display, "XSEL_DATA", False),
incrid = XInternAtom(display, "INCR", False);
XEvent event;
XSelectInput (display, window, PropertyChangeMask);
XConvertSelection(display, bufid, fmtid, propid, window, CurrentTime);
do {
XNextEvent(display, &event);
} while (event.type != SelectionNotify || event.xselection.selection != bufid);
if (event.xselection.property)
{
XGetWindowProperty(display, window, propid, 0, LONG_MAX/4, True, AnyPropertyType,
&fmtid, &resbits, &ressize, &restail, (unsigned char**)&result);
if (fmtid != incrid)
printf("%.*s", (int)ressize, result);
XFree(result);
if (fmtid == incrid)
do {
do {
XNextEvent(display, &event);
} while (event.type != PropertyNotify || event.xproperty.atom != propid || event.xproperty.state != PropertyNewValue);
XGetWindowProperty(display, window, propid, 0, LONG_MAX/4, True, AnyPropertyType,
&fmtid, &resbits, &ressize, &restail, (unsigned char**)&result);
printf("%.*s", (int)ressize, result);
XFree(result);
} while (ressize > 0);
return True;
}
else // request failed, e.g. owner can't convert to the target format
return False;
}
int main()
{
Display *display = XOpenDisplay(NULL);
unsigned long color = BlackPixel(display, DefaultScreen(display));
Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 0,0, 1,1, 0, color, color);
Bool result = PrintSelection(display, window, "CLIPBOARD", "UTF8_STRING") ||
PrintSelection(display, window, "CLIPBOARD", "STRING");
XDestroyWindow(display, window);
XCloseDisplay(display);
return !result;
}
For example xsel
tool uses INCR
transfer for buffers larger than 4000. According to ICCCM, it's up to the application to choose a reasonable size limit.
The same code works for PRIMARY
selection. Replace "CLIPBOARD" with "PRIMARY" to print PRIMARY
selection contents.
References