C++ Get string from Clipboard on Linux

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

  • X Selections summary by Jamie Zawinski
  • Xlib Programming Manual - Selections
  • ICCCM - Large Data Transfers and INCR protocol
  • https://github.com/exebook/x11clipboard - minimal XCopy() and XPaste() implementations
  • xsel and xclip sources
  • The Secondary Selection - history and ideas by Charles Lindsey

Did you try to find not a code first but a program with an implementation ? I did it for you and found a lot of implementations which use direct X11 calls. I think the most valuable is this but also you may read this. Just find any program and look for the sources. Try to look on wikipedia what applications use x11 clipboard/selection system.

The following programs specifically operate on data transfer mechanisms:

xcutsel transfers data from selections to cut buffers or vice versa

xclipboard, glipper (Gnome), parcellite (LXDE), and klipper (KDE) are clipboard managers, maybe wmcliphist as well xcb shows the content of the cut buffers and allows the user to manipulate them xselection,

xclip, xsel and xcopy are command line programs that copy data to or from the X selection. xcopy has a verbosity option that helps debug X selection issues. parcellite also has the ability to read from and write to specific X selections from the command line.

synergy is a cross platform tool that allows you to share a clipboard across multiple computers running multiple operating systems

xfce4-clipman-plugin is a "clipboard history plugin for the Xfce4 panel" and also a clipboard manager xtranslate looks up words in the Xselection in a multi-lingual dictionary autocutsel syncs cut buffer and selection buffer

Shortly, in theory, X11 has 2 "clipboards": actually a keyboard and for selections - the text you selected immediately can be pasted anywhere you want by pressing middle-mouse button while actual "keyboard" is made for main/default clipboard purposes as exchange by different kind of objects.

P.S. I'd not work with x11 anymore after my experience. Enjoy :)