commit 5bcc196ebfd966a0d6479164d02e4a05d10b38fa
parent 004ac7555f2e18a6f439e80a8aca23e52f838621
Author: lumidify <nobody@lumidify.org>
Date:   Mon, 21 Aug 2023 20:25:53 +0200
Add clipboard support to text entry
Diffstat:
16 files changed, 2411 insertions(+), 23 deletions(-)
diff --git a/.ltk/ltk.cfg b/.ltk/ltk.cfg
@@ -41,6 +41,8 @@ bind-keypress delete-char-backwards sym backspace
 bind-keypress delete-char-forwards sym delete
 bind-keypress expand-selection-left sym left mods shift
 bind-keypress expand-selection-right sym right mods shift
+bind-keypress selection-to-clipboard text c mods ctrl
+bind-keypress paste-clipboard text v mods ctrl
 
 # default mapping (just to silence warnings)
 [key-mapping]
diff --git a/LICENSE b/LICENSE
@@ -1,10 +1,10 @@
-See src/khash.h, src/ini.*, src/stb_truetype.*, and src/strtonum.c
-for third-party licenses.
+See src/khash.h, src/ini.*, src/stb_truetype.*, src/strtonum.c,
+and src/ctrlsel.* for third-party licenses.
 
 ISC License
 
 The Lumidify ToolKit (LTK)
-Copyright (c) 2016-2022 lumidify <nobody@lumidify.org>
+Copyright (c) 2016-2023 lumidify <nobody@lumidify.org>
 
 Permission to use, copy, modify, and/or distribute this software for any
 purpose with or without fee is hereby granted, provided that the above
diff --git a/Makefile b/Makefile
@@ -35,8 +35,8 @@ EXTRA_OBJ = $(EXTRA_OBJ_$(USE_PANGO))
 EXTRA_CFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_CFLAGS_$(DEV)) $(EXTRA_CFLAGS_$(USE_PANGO))
 EXTRA_LDFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFLAGS_$(USE_PANGO))
 
-LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -std=c99 `pkg-config --cflags x11 fontconfig xext` -D_POSIX_C_SOURCE=200809L
-LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext`
+LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -std=c99 `pkg-config --cflags x11 fontconfig xext xcursor` -D_POSIX_C_SOURCE=200809L
+LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext xcursor`
 
 OBJ = \
 	src/strtonum.o \
@@ -60,6 +60,9 @@ OBJ = \
 	src/event_xlib.o \
 	src/err.o \
 	src/config.o \
+	src/clipboard_xlib.o \
+	src/txtbuf.o \
+	src/ctrlsel.o \
 	$(EXTRA_OBJ)
 
 # Note: This could be improved so a change in a header only causes the .c files
@@ -94,7 +97,11 @@ HDR = \
 	src/proto_types.h \
 	src/config.h \
 	src/array.h \
-	src/keys.h
+	src/keys.h \
+	src/clipboard_xlib.h \
+	src/clipboard.h \
+	src/txtbuf.h \
+	src/ctrlsel.h
 
 all: src/ltkd src/ltkc
 
diff --git a/src/clipboard.h b/src/clipboard.h
@@ -0,0 +1,26 @@
+#ifndef LTK_CLIPBOARD_H
+#define LTK_CLIPBOARD_H
+
+#include "txtbuf.h"
+#include "graphics.h"
+
+typedef struct ltk_clipboard ltk_clipboard;
+
+ltk_clipboard *ltk_clipboard_create(ltk_renderdata *data);
+void ltk_clipboard_destroy(ltk_clipboard *clip);
+void ltk_clipboard_set_primary_text(ltk_clipboard *clip, char *text);
+txtbuf *ltk_clipboard_get_primary_buffer(ltk_clipboard *clip);
+void ltk_clipboard_set_primary_selection_owner(ltk_clipboard *clip);
+void ltk_clipboard_set_clipboard_text(ltk_clipboard *clip, char *text);
+txtbuf *ltk_clipboard_get_clipboard_buffer(ltk_clipboard *clip);
+void ltk_clipboard_set_clipboard_selection_owner(ltk_clipboard *clip);
+void ltk_clipboard_primary_to_clipboard(ltk_clipboard *clip);
+
+/* FIXME: configure timeout for getting text */
+/* WARNING: The returned txtbuf is owned by the clipboard and must
+   be copied before further processing and especially before any
+   further clipboard functions are called. */
+txtbuf *ltk_clipboard_get_clipboard_text(ltk_clipboard *clip);
+txtbuf *ltk_clipboard_get_primary_text(ltk_clipboard *clip);
+
+#endif /* LTK_CLIPBOARD_H */
diff --git a/src/clipboard_xlib.c b/src/clipboard_xlib.c
@@ -0,0 +1,252 @@
+/* Copied almost exactly from ledit. */
+
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+
+#include "util.h"
+#include "memory.h"
+#include "graphics.h"
+#include "clipboard.h"
+#include "clipboard_xlib.h"
+#include "xlib_shared.h"
+#include "macros.h"
+#include "config.h"
+#include "ctrlsel.h"
+
+/* Some *inspiration* taken from SDL (https://libsdl.org), mainly
+   the idea to create a separate window just for clipboard handling. */
+
+static Window get_clipboard_window(ltk_clipboard *clip);
+static Bool check_window(Display *dpy, XEvent *event, XPointer arg);
+static txtbuf *get_text(ltk_clipboard *clip, int primary);
+
+struct ltk_clipboard {
+	txtbuf *primary;
+	txtbuf *clipboard;
+	txtbuf *rbuf;
+	ltk_renderdata *renderdata;
+	Window window;
+	struct CtrlSelTarget starget;
+	struct CtrlSelTarget rtarget;
+	CtrlSelContext *scontext;
+	Atom xtarget;
+};
+
+ltk_clipboard *
+ltk_clipboard_create(ltk_renderdata *renderdata) {
+	ltk_clipboard *clip = ltk_malloc(sizeof(ltk_clipboard));
+	clip->primary = txtbuf_new();
+	clip->clipboard = txtbuf_new();
+	clip->rbuf = txtbuf_new();
+	clip->renderdata = renderdata;
+	clip->window = None;
+	clip->xtarget = None;
+	#ifdef X_HAVE_UTF8_STRING
+	clip->xtarget = XInternAtom(renderdata->dpy, "UTF8_STRING", False);
+	#else
+	clip->xtarget = XA_STRING;
+	#endif
+	clip->scontext = NULL;
+	return clip;
+}
+
+void
+ltk_clipboard_destroy(ltk_clipboard *clip) {
+	txtbuf_destroy(clip->primary);
+	txtbuf_destroy(clip->clipboard);
+	txtbuf_destroy(clip->rbuf);
+	if (clip->scontext)
+		ctrlsel_disown(clip->scontext);
+	if (clip->window != None)
+		XDestroyWindow(clip->renderdata->dpy, clip->window);
+	free(clip);
+}
+
+static Window
+get_clipboard_window(ltk_clipboard *clip) {
+	if (clip->window == None) {
+		clip->window = XCreateWindow(
+		    clip->renderdata->dpy, DefaultRootWindow(clip->renderdata->dpy),
+		    -10, -10, 1, 1, 0, CopyFromParent, InputOnly, CopyFromParent, 0, NULL
+		);
+		XFlush(clip->renderdata->dpy);
+	}
+	return clip->window;
+}
+
+void
+ltk_clipboard_set_primary_text(ltk_clipboard *clip, char *text) {
+	txtbuf_set_text(clip->primary, text);
+	ltk_clipboard_set_primary_selection_owner(clip);
+}
+
+txtbuf *
+ltk_clipboard_get_primary_buffer(ltk_clipboard *clip) {
+	return clip->primary;
+}
+
+void
+ltk_clipboard_set_primary_selection_owner(ltk_clipboard *clip) {
+	Window window = get_clipboard_window(clip);
+	if (clip->scontext)
+		ctrlsel_disown(clip->scontext);
+	clip->scontext = NULL;
+	/* FIXME: is it fine to cast to unsigned char everywhere? */
+	ctrlsel_filltarget(clip->xtarget, clip->xtarget, 8, (unsigned char *)clip->primary->text, clip->primary->len, &clip->starget);
+	/* FIXME: use proper time */
+	clip->scontext = ctrlsel_setowner(clip->renderdata->dpy, window, XA_PRIMARY, CurrentTime, 0, &clip->starget, 1);
+	if (!clip->scontext)
+		fprintf(stderr, "WARNING: Could not own primary selection.\n");
+}
+
+void
+ltk_clipboard_set_clipboard_text(ltk_clipboard *clip, char *text) {
+	txtbuf_set_text(clip->clipboard, text);
+	ltk_clipboard_set_clipboard_selection_owner(clip);
+}
+
+txtbuf *
+ltk_clipboard_get_clipboard_buffer(ltk_clipboard *clip) {
+	return clip->clipboard;
+}
+
+void
+ltk_clipboard_set_clipboard_selection_owner(ltk_clipboard *clip) {
+	Atom clip_atom;
+	Window window = get_clipboard_window(clip);
+	clip_atom = XInternAtom(clip->renderdata->dpy, "CLIPBOARD", False);
+	if (clip->scontext)
+		ctrlsel_disown(clip->scontext);
+	clip->scontext = NULL;
+	/* FIXME: see clipboard_set_primary_selection_owner */
+	ctrlsel_filltarget(clip->xtarget, clip->xtarget, 8, (unsigned char *)clip->clipboard->text, clip->clipboard->len, &clip->starget);
+	/* FIXME: use proper time */
+	clip->scontext = ctrlsel_setowner(clip->renderdata->dpy, window, clip_atom, CurrentTime, 0, &clip->starget, 1);
+	if (!clip->scontext)
+		fprintf(stderr, "WARNING: Could not own clipboard selection.\n");
+}
+
+void
+ltk_clipboard_primary_to_clipboard(ltk_clipboard *clip) {
+	if (clip->primary->len > 0) {
+		txtbuf_copy(clip->clipboard, clip->primary);
+		ltk_clipboard_set_clipboard_selection_owner(clip);
+	}
+}
+
+int
+ltk_clipboard_filter_event(ltk_clipboard *clip, XEvent *e) {
+	if (clip->window != None && e->xany.window == clip->window) {
+		if (clip->scontext)
+			ctrlsel_send(clip->scontext, e);
+		/* other events are discarded since there
+		   was no request to get the clipboard text */
+		return 1;
+	}
+	return 0;
+}
+
+static Bool
+check_window(Display *dpy, XEvent *event, XPointer arg) {
+	(void)dpy;
+	return *(Window *)arg == event->xany.window;
+}
+
+/* WARNING: The returned txtbuf needs to be copied before further processing! */
+static txtbuf *
+get_text(ltk_clipboard *clip, int primary) {
+	CtrlSelContext *context;
+	Window window = get_clipboard_window(clip);
+	ctrlsel_filltarget(clip->xtarget, clip->xtarget, 0, NULL, 0, &clip->rtarget);
+	Atom clip_atom = primary ? XA_PRIMARY : XInternAtom(clip->renderdata->dpy, "CLIPBOARD", False);
+	/* FIXME: use proper time here */
+	context = ctrlsel_request(clip->renderdata->dpy, window, clip_atom, CurrentTime, &clip->rtarget, 1);
+	/* FIXME: show error in window? */
+	if (!context) {
+		fprintf(stderr, "WARNING: Unable to request selection.\n");
+		return NULL;
+	}
+
+	struct timespec now, elapsed, last, start, sleep_time;
+	sleep_time.tv_sec = 0;
+	clock_gettime(CLOCK_MONOTONIC, &start);
+	last = start;
+	XEvent event;
+	while (1) {
+		/* FIXME: I have no idea how inefficient this is */
+		if (XCheckIfEvent(clip->renderdata->dpy, &event, &check_window, (XPointer)&window)) {
+			switch (ctrlsel_receive(context, &event)) {
+			case CTRLSEL_RECEIVED:
+				goto done;
+			case CTRLSEL_ERROR:
+				fprintf(stderr, "WARNING: Could not get selection.\n");
+				ctrlsel_cancel(context);
+				return NULL;
+			default:
+				continue;
+			}
+		}
+		clock_gettime(CLOCK_MONOTONIC, &now);
+		ltk_timespecsub(&now, &start, &elapsed);
+		/* Timeout if it takes too long. When that happens, become the selection owner to
+		   avoid further timeouts in the future (I think I copied this behavior from SDL). */
+		/* FIXME: configure timeout */
+		if (elapsed.tv_sec > 0) {
+			if (primary)
+				ltk_clipboard_set_primary_text(clip, "");
+			else
+				ltk_clipboard_set_clipboard_text(clip, "");
+			return NULL;
+		}
+		ltk_timespecsub(&now, &last, &elapsed);
+		/* FIXME: configure nanoseconds */
+		if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000) {
+			sleep_time.tv_nsec = 20000000 - elapsed.tv_nsec;
+			nanosleep(&sleep_time, NULL);
+		}
+		last = now;
+	}
+	return NULL;
+done:
+	/* FIXME: this is a bit ugly because it fiddles around with txtbuf internals */
+	free(clip->rbuf->text);
+	clip->rbuf->cap = clip->rbuf->len = clip->rtarget.bufsize;
+	/* FIXME: again weird conversion between char and unsigned char */
+	clip->rbuf->text = (char *)clip->rtarget.buffer;
+	clip->rtarget.buffer = NULL; /* important so ctrlsel_cancel doesn't free it */
+	ctrlsel_cancel(context);
+	return clip->rbuf;
+}
+
+txtbuf *
+ltk_clipboard_get_clipboard_text(ltk_clipboard *clip) {
+	Atom clip_atom;
+	clip_atom = XInternAtom(clip->renderdata->dpy, "CLIPBOARD", False);
+	Window window = get_clipboard_window(clip);
+	Window owner = XGetSelectionOwner(clip->renderdata->dpy, clip_atom);
+	if (owner == None) {
+		return NULL;
+	} else if (owner == window) {
+		return clip->clipboard;
+	} else {
+		return get_text(clip, 0);
+	}
+}
+
+txtbuf *
+ltk_clipboard_get_primary_text(ltk_clipboard *clip) {
+	Window window = get_clipboard_window(clip);
+	Window owner = XGetSelectionOwner(clip->renderdata->dpy, XA_PRIMARY);
+	if (owner == None) {
+		return NULL;
+	} else if (owner == window) {
+		return clip->primary;
+	} else {
+		return get_text(clip, 1);
+	}
+}
diff --git a/src/clipboard_xlib.h b/src/clipboard_xlib.h
@@ -0,0 +1,11 @@
+#ifndef LTK_CLIPBOARD_XLIB_H
+#define LTK_CLIPBOARD_XLIB_H
+
+#include <X11/Xlib.h>
+#include "clipboard.h"
+#include "txtbuf.h"
+
+/* 1 means the event was used by the clipboard, 0 means it wasn't */
+int ltk_clipboard_filter_event(ltk_clipboard *clip, XEvent *e);
+
+#endif /* LTK_CLIPBOARD_XLIB_H */
diff --git a/src/ctrlsel.c b/src/ctrlsel.c
@@ -0,0 +1,1645 @@
+/*
+ * MIT/X Consortium License
+ *
+ * © 2022-2023 Lucas de Sena <lucas at seninha dot org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+#include <X11/keysym.h>
+#include <X11/cursorfont.h>
+#include <X11/Xcursor/Xcursor.h>
+
+#include "ctrlsel.h"
+
+#define _TIMESTAMP_PROP "_TIMESTAMP_PROP"
+#define TIMESTAMP       "TIMESTAMP"
+#define ATOM_PAIR       "ATOM_PAIR"
+#define MULTIPLE        "MULTIPLE"
+#define MANAGER         "MANAGER"
+#define TARGETS         "TARGETS"
+#define INCR            "INCR"
+#define SELDEFSIZE      0x4000
+#define FLAG(f, b)      (((f) & (b)) == (b))
+#define MOTION_TIME     32
+#define DND_DISTANCE    8               /* distance from pointer to dnd miniwindow */
+#define XDND_VERSION    5               /* XDND protocol version */
+#define NCLIENTMSG_DATA 5               /* number of members on a the .data.l[] array of a XClientMessageEvent */
+
+enum {
+	CONTENT_INCR,
+	CONTENT_ZERO,
+	CONTENT_ERROR,
+	CONTENT_SUCCESS,
+};
+
+enum {
+	PAIR_TARGET,
+	PAIR_PROPERTY,
+	PAIR_LAST
+};
+
+enum {
+	/* xdnd window properties */
+	XDND_AWARE,
+
+	/* xdnd selections */
+	XDND_SELECTION,
+
+	/* xdnd client messages */
+	XDND_ENTER,
+	XDND_POSITION,
+	XDND_STATUS,
+	XDND_LEAVE,
+	XDND_DROP,
+	XDND_FINISHED,
+
+	/* xdnd actions */
+	XDND_ACTION_COPY,
+	XDND_ACTION_MOVE,
+	XDND_ACTION_LINK,
+	XDND_ACTION_ASK,
+	XDND_ACTION_PRIVATE,
+
+	XDND_ATOM_LAST,
+};
+
+enum {
+	CURSOR_TARGET,
+	CURSOR_PIRATE,
+	CURSOR_DRAG,
+	CURSOR_COPY,
+	CURSOR_MOVE,
+	CURSOR_LINK,
+	CURSOR_NODROP,
+	CURSOR_LAST,
+};
+
+struct Transfer {
+	/*
+ 	 * When a client request the clipboard but its content is too
+ 	 * large, we perform incremental transfer.  We keep track of
+ 	 * each incremental transfer in a list of transfers.
+ 	 */
+	struct Transfer *prev, *next;
+	struct CtrlSelTarget *target;
+	Window requestor;
+	Atom property;
+	unsigned long size;     /* how much have we transferred */
+};
+
+struct PredArg {
+	CtrlSelContext *context;
+	Window window;
+	Atom message_type;
+};
+
+struct CtrlSelContext {
+	Display *display;
+	Window window;
+	Atom selection;
+	Time time;
+	unsigned long ntargets;
+	struct CtrlSelTarget *targets;
+
+	/*
+	 * Items below are used internally to keep track of any
+	 * incremental transference in progress.
+	 */
+	unsigned long selmaxsize;
+	unsigned long ndone;
+	void *transfers;
+
+	/*
+	 * Items below are used internally for drag-and-dropping.
+	 */
+	Window dndwindow;
+	unsigned int dndactions, dndresult;
+};
+
+static char *atomnames[XDND_ATOM_LAST] = {
+	[XDND_AWARE]                 = "XdndAware",
+	[XDND_SELECTION]             = "XdndSelection",
+	[XDND_ENTER]                 = "XdndEnter",
+	[XDND_POSITION]              = "XdndPosition",
+	[XDND_STATUS]                = "XdndStatus",
+	[XDND_LEAVE]                 = "XdndLeave",
+	[XDND_DROP]                  = "XdndDrop",
+	[XDND_FINISHED]              = "XdndFinished",
+	[XDND_ACTION_COPY]           = "XdndActionCopy",
+	[XDND_ACTION_MOVE]           = "XdndActionMove",
+	[XDND_ACTION_LINK]           = "XdndActionLink",
+	[XDND_ACTION_ASK]            = "XdndActionAsk",
+	[XDND_ACTION_PRIVATE]        = "XdndActionPrivate",
+};
+
+static int
+between(int x, int y, int x0, int y0, int w0, int h0)
+{
+	return x >= x0 && x < x0 + w0 && y >= y0 && y < y0 + h0;
+}
+
+static void
+clientmsg(Display *dpy, Window win, Atom atom, long d[5])
+{
+	XEvent ev;
+
+	ev.xclient.type = ClientMessage;
+	ev.xclient.display = dpy;
+	ev.xclient.serial = 0;
+	ev.xclient.send_event = True;
+	ev.xclient.message_type = atom;
+	ev.xclient.window = win;
+	ev.xclient.format = 32;
+	ev.xclient.data.l[0] = d[0];
+	ev.xclient.data.l[1] = d[1];
+	ev.xclient.data.l[2] = d[2];
+	ev.xclient.data.l[3] = d[3];
+	ev.xclient.data.l[4] = d[4];
+	(void)XSendEvent(dpy, win, False, 0x0, &ev);
+}
+
+static unsigned long
+getselmaxsize(Display *display)
+{
+	unsigned long n;
+
+	if ((n = XExtendedMaxRequestSize(display)) > 0)
+		return n;
+	if ((n = XMaxRequestSize(display)) > 0)
+		return n;
+	return SELDEFSIZE;
+}
+
+static int
+getservertime(Display *display, Time *time)
+{
+	XEvent xev;
+	Window window;
+	Atom timeprop;
+
+	/*
+	 * According to ICCCM, a client wishing to acquire ownership of
+	 * a selection should set the specfied time to some time between
+	 * the current last-change time of the selection concerned and
+	 * the current server time.
+	 *
+	 * Those clients should not set the time value to `CurrentTime`,
+	 * because if they do so, they have no way of finding when they
+	 * gained ownership of the selection.
+	 *
+	 * In the case that an event triggers the acquisition of the
+	 * selection, this time value can be obtained from the event
+	 * itself.
+	 *
+	 * In the case that the client must unconditionally acquire the
+	 * ownership of a selection (which is our case), a zero-length
+	 * append to a property is a way to obtain a timestamp for this
+	 * purpose.  The timestamp is in the corresponding
+	 * `PropertyNotify` event.
+	 */
+
+	if (time != CurrentTime)
+		return 1;
+	timeprop = XInternAtom(display, _TIMESTAMP_PROP, False);
+	if (timeprop == None)
+		goto error;
+	window = XCreateWindow(
+		display,
+		DefaultRootWindow(display),
+		0, 0, 1, 1, 0,
+		CopyFromParent, CopyFromParent, CopyFromParent,
+		CWEventMask,
+		&(XSetWindowAttributes){
+			.event_mask = PropertyChangeMask,
+		}
+	);
+	if (window == None)
+		goto error;
+	XChangeProperty(
+		display, window,
+		timeprop, timeprop,
+		8L, PropModeAppend, NULL, 0
+	);
+	while (!XWindowEvent(display, window, PropertyChangeMask, &xev)) {
+		if (xev.type == PropertyNotify &&
+		    xev.xproperty.window == window &&
+		    xev.xproperty.atom == timeprop) {
+			*time = xev.xproperty.time;
+			break;
+		}
+	}
+	(void)XDestroyWindow(display, window);
+	return 1;
+error:
+	return 0;
+}
+
+static int
+nbytes(int format)
+{
+	switch (format) {
+	default: return sizeof(char);
+	case 16: return sizeof(short);
+	case 32: return sizeof(long);
+	}
+}
+
+static int
+getcontent(struct CtrlSelTarget *target, Display *display, Window window, Atom property)
+{
+	unsigned char *p, *q;
+	unsigned long len, addsize, size;
+	unsigned long dl;   /* dummy variable */
+	int status;
+	Atom incr;
+
+	incr = XInternAtom(display, INCR, False),
+	status = XGetWindowProperty(
+		display,
+		window,
+		property,
+		0L, 0x1FFFFFFF,
+		True,
+		AnyPropertyType,
+		&target->type,
+		&target->format,
+		&len, &dl, &p
+	);
+	if (target->format != 32 && target->format != 16)
+		target->format = 8;
+	if (target->type == incr) {
+		XFree(p);
+		return CONTENT_INCR;
+	}
+	if (len == 0) {
+		XFree(p);
+		return CONTENT_ZERO;
+	}
+	if (status != Success) {
+		XFree(p);
+		return CONTENT_ERROR;
+	}
+	if (p == NULL) {
+		XFree(p);
+		return CONTENT_ERROR;
+	}
+	addsize = len * nbytes(target->format);
+	size = addsize;
+	if (target->buffer != NULL) {
+		/* append buffer */
+		size += target->bufsize;
+		if ((q = realloc(target->buffer, size + 1)) == NULL) {
+			XFree(p);
+			return CONTENT_ERROR;
+		}
+		memcpy(q + target->bufsize, p, addsize);
+		target->buffer = q;
+		target->bufsize = size;
+		target->nitems += len;
+	} else {
+		/* new buffer */
+		if ((q = malloc(size + 1)) == NULL) {
+			XFree(p);
+			return CONTENT_ERROR;
+		}
+		memcpy(q, p, addsize);
+		target->buffer = q;
+		target->bufsize = size;
+		target->nitems = len;
+	}
+	target->buffer[size] = '\0';
+	XFree(p);
+	return CONTENT_SUCCESS;
+}
+
+static void
+deltransfer(CtrlSelContext *context, struct Transfer *transfer)
+{
+	if (transfer->prev != NULL) {
+		transfer->prev->next = transfer->next;
+	} else {
+		context->transfers = transfer->next;
+	}
+	if (transfer->next != NULL) {
+		transfer->next->prev = transfer->prev;
+	}
+}
+
+static void
+freetransferences(CtrlSelContext *context)
+{
+	struct Transfer *transfer;
+
+	while (context->transfers != NULL) {
+		transfer = (struct Transfer *)context->transfers;
+		context->transfers = ((struct Transfer *)context->transfers)->next;
+		XDeleteProperty(
+			context->display,
+			transfer->requestor,
+			transfer->property
+		);
+		free(transfer);
+	}
+	context->transfers = NULL;
+}
+
+static void
+freebuffers(CtrlSelContext *context)
+{
+	unsigned long i;
+
+	for (i = 0; i < context->ntargets; i++) {
+		free(context->targets[i].buffer);
+		context->targets[i].buffer = NULL;
+		context->targets[i].nitems = 0;
+		context->targets[i].bufsize = 0;
+	}
+}
+
+static unsigned long
+getatomsprop(Display *display, Window window, Atom property, Atom type, Atom **atoms)
+{
+	unsigned char *p;
+	unsigned long len;
+	unsigned long dl;       /* dummy variable */
+	int format;
+	Atom gottype;
+	unsigned long size;
+	int success;
+
+	success = XGetWindowProperty(
+		display,
+		window,
+		property,
+		0L, 0x1FFFFFFF,
+		False,
+		type, &gottype,
+		&format, &len,
+		&dl, &p
+	);
+	if (success != Success || len == 0 || p == NULL || format != 32)
+		goto error;
+	if (type != AnyPropertyType && type != gottype)
+		goto error;
+	size = len * sizeof(**atoms);
+	if ((*atoms = malloc(size)) == NULL)
+		goto error;
+	memcpy(*atoms, p, size);
+	XFree(p);
+	return len;
+error:
+	XFree(p);
+	*atoms = NULL;
+	return 0;
+}
+
+static int
+newtransfer(CtrlSelContext *context, struct CtrlSelTarget *target, Window requestor, Atom property)
+{
+	struct Transfer *transfer;
+
+	transfer = malloc(sizeof(*transfer));
+	if (transfer == NULL)
+		return 0;
+	*transfer = (struct Transfer){
+		.prev = NULL,
+		.next = (struct Transfer *)context->transfers,
+		.requestor = requestor,
+		.property = property,
+		.target = target,
+		.size = 0,
+	};
+	if (context->transfers != NULL)
+		((struct Transfer *)context->transfers)->prev = transfer;
+	context->transfers = transfer;
+	return 1;
+}
+
+static Bool
+convert(CtrlSelContext *context, Window requestor, Atom target, Atom property)
+{
+	Atom multiple, timestamp, targets, incr;
+	Atom *supported;
+	unsigned long i;
+	int nsupported;
+
+	incr = XInternAtom(context->display, INCR, False);
+	targets = XInternAtom(context->display, TARGETS, False);
+	multiple = XInternAtom(context->display, MULTIPLE, False);
+	timestamp = XInternAtom(context->display, TIMESTAMP, False);
+	if (target == multiple) {
+		/* A MULTIPLE should be handled when processing a
+		 * SelectionRequest event.  We do not support nested
+		 * MULTIPLE targets.
+		 */
+		return False;
+	}
+	if (target == timestamp) {
+		/*
+		 * According to ICCCM, to avoid some race conditions, it
+		 * is important that requestors be able to discover the
+		 * timestamp the owner used to acquire ownership.
+		 * Requestors do that by requesting selection owners to
+		 * convert the `TIMESTAMP` target.  Selection owners
+		 * must return the timestamp as an `XA_INTEGER`.
+		 */
+		XChangeProperty(
+			context->display,
+			requestor,
+			property,
+			XA_INTEGER, 32,
+			PropModeReplace,
+			(unsigned char *)&context->time,
+			1
+		);
+		return True;
+	}
+	if (target == targets) {
+		/*
+		 * According to ICCCM, when requested for the `TARGETS`
+		 * target, the selection owner should return a list of
+		 * atoms representing the targets for which an attempt
+		 * to convert the selection will (hopefully) succeed.
+		 */
+		nsupported = context->ntargets + 2;     /* +2 for MULTIPLE + TIMESTAMP */
+		if ((supported = calloc(nsupported, sizeof(*supported))) == NULL)
+			return False;
+		for (i = 0; i < context->ntargets; i++) {
+			supported[i] = context->targets[i].target;
+		}
+		supported[i++] = multiple;
+		supported[i++] = timestamp;
+		XChangeProperty(
+			context->display,
+			requestor,
+			property,
+			XA_ATOM, 32,
+			PropModeReplace,
+			(unsigned char *)supported,
+			nsupported
+		);
+		free(supported);
+		return True;
+	}
+	for (i = 0; i < context->ntargets; i++) {
+		if (target == context->targets[i].target)
+			goto found;
+	}
+	return False;
+found:
+	if (context->targets[i].bufsize > context->selmaxsize) {
+		XSelectInput(
+			context->display,
+			requestor,
+			StructureNotifyMask | PropertyChangeMask
+		);
+		XChangeProperty(
+			context->display,
+			requestor,
+			property,
+			incr,
+			32L,
+			PropModeReplace,
+			(unsigned char *)context->targets[i].buffer,
+			1
+		);
+		newtransfer(context, &context->targets[i], requestor, property);
+	} else {
+		XChangeProperty(
+			context->display,
+			requestor,
+			property,
+			target,
+			context->targets[i].format,
+			PropModeReplace,
+			context->targets[i].buffer,
+			context->targets[i].nitems
+		);
+	}
+	return True;
+}
+
+static int
+request(CtrlSelContext *context)
+{
+	Atom multiple, atom_pair;
+	Atom *pairs;
+	unsigned long i, size;
+
+	for (i = 0; i < context->ntargets; i++) {
+		context->targets[i].nitems = 0;
+		context->targets[i].bufsize = 0;
+		context->targets[i].buffer = NULL;
+	}
+	if (context->ntargets == 1) {
+		(void)XConvertSelection(
+			context->display,
+			context->selection,
+			context->targets[0].target,
+			context->targets[0].target,
+			context->window,
+			context->time
+		);
+	} else if (context->ntargets > 1) {
+		multiple = XInternAtom(context->display, MULTIPLE, False);
+		atom_pair = XInternAtom(context->display, ATOM_PAIR, False);
+		size = 2 * context->ntargets;
+		pairs = calloc(size, sizeof(*pairs));
+		if (pairs == NULL)
+			return 0;
+		for (i = 0; i < context->ntargets; i++) {
+			pairs[i * 2 + 0] = context->targets[i].target;
+			pairs[i * 2 + 1] = context->targets[i].target;
+		}
+		(void)XChangeProperty(
+			context->display,
+			context->window,
+			multiple,
+			atom_pair,
+			32,
+			PropModeReplace,
+			(unsigned char *)pairs,
+			size
+		);
+		(void)XConvertSelection(
+			context->display,
+			context->selection,
+			multiple,
+			multiple,
+			context->window,
+			context->time
+		);
+		free(pairs);
+	}
+	return 1;
+}
+
+void
+ctrlsel_filltarget(
+	Atom target,
+	Atom type,
+	int format,
+	unsigned char *buffer,
+	unsigned long size,
+	struct CtrlSelTarget *fill
+) {
+	if (fill == NULL)
+		return;
+	if (format != 32 && format != 16)
+		format = 8;
+	*fill = (struct CtrlSelTarget){
+		.target = target,
+		.type = type,
+		.action = None,
+		.format = format,
+		.nitems = size / nbytes(format),
+		.buffer = buffer,
+		.bufsize = size,
+	};
+}
+
+CtrlSelContext *
+ctrlsel_request(
+	Display *display,
+	Window window,
+	Atom selection,
+	Time time,
+	struct CtrlSelTarget targets[],
+	unsigned long ntargets
+) {
+	CtrlSelContext *context;
+
+	if (!getservertime(display, &time))
+		return NULL;
+	if ((context = malloc(sizeof(*context))) == NULL)
+		return NULL;
+	*context = (CtrlSelContext){
+		.display = display,
+		.window = window,
+		.selection = selection,
+		.time = time,
+		.targets = targets,
+		.ntargets = ntargets,
+		.selmaxsize = getselmaxsize(display),
+		.ndone = 0,
+		.transfers = NULL,
+		.dndwindow = None,
+		.dndactions = 0x00,
+		.dndresult = 0x00,
+	};
+	if (ntargets == 0)
+		return context;
+	if (request(context))
+		return context;
+	free(context);
+	return NULL;
+}
+
+CtrlSelContext *
+ctrlsel_setowner(
+	Display *display,
+	Window window,
+	Atom selection,
+	Time time,
+	int ismanager,
+	struct CtrlSelTarget targets[],
+	unsigned long ntargets
+) {
+	CtrlSelContext *context;
+	Window root;
+
+	root = DefaultRootWindow(display);
+	if (!getservertime(display, &time))
+		return NULL;
+	if ((context = malloc(sizeof(*context))) == NULL)
+		return NULL;
+	*context = (CtrlSelContext){
+		.display = display,
+		.window = window,
+		.selection = selection,
+		.time = time,
+		.targets = targets,
+		.ntargets = ntargets,
+		.selmaxsize = getselmaxsize(display),
+		.ndone = 0,
+		.transfers = NULL,
+		.dndwindow = None,
+		.dndactions = 0x00,
+		.dndresult = 0x00,
+	};
+	(void)XSetSelectionOwner(display, selection, window, time);
+	if (XGetSelectionOwner(display, selection) != window) {
+		free(context);
+		return NULL;
+	}
+	if (!ismanager)
+		return context;
+
+	/*
+	 * According to ICCCM, a manager client (that is, a client
+	 * responsible for managing shared resources) should take
+	 * ownership of an appropriate selection.
+	 *
+	 * Immediately after a manager successfully acquires ownership
+	 * of a manager selection, it should announce its arrival by
+	 * sending a `ClientMessage` event.  (That is necessary for
+	 * clients to be able to know when a specific manager has
+	 * started: any client that wish to do so should select for
+	 * `StructureNotify` on the root window and should watch for
+	 * the appropriate `MANAGER` `ClientMessage`).
+	 */
+	(void)XSendEvent(
+		display,
+		root,
+		False,
+		StructureNotifyMask,
+		(XEvent *)&(XClientMessageEvent){
+			.type         = ClientMessage,
+			.window       = root,
+			.message_type = XInternAtom(display, MANAGER, False),
+			.format       = 32,
+			.data.l[0]    = time,           /* timestamp */
+			.data.l[1]    = selection,      /* manager selection atom */
+			.data.l[2]    = window,         /* window owning the selection */
+			.data.l[3]    = 0,              /* manager-specific data */
+			.data.l[4]    = 0,              /* manager-specific data */
+		}
+	);
+	return context;
+}
+
+static int
+receiveinit(CtrlSelContext *context, XEvent *xev)
+{
+	struct CtrlSelTarget *targetp;
+	XSelectionEvent *xselev;
+	Atom multiple, atom_pair;
+	Atom *pairs;
+	Atom pair[PAIR_LAST];
+	unsigned long j, natoms;
+	unsigned long i;
+	int status, success;
+
+	multiple = XInternAtom(context->display, MULTIPLE, False);
+	atom_pair = XInternAtom(context->display, ATOM_PAIR, False);
+	xselev = &xev->xselection;
+	if (xselev->selection != context->selection)
+		return CTRLSEL_NONE;
+	if (xselev->requestor != context->window)
+		return CTRLSEL_NONE;
+	if (xselev->property == None)
+		return CTRLSEL_ERROR;
+	if (xselev->target == multiple) {
+		natoms = getatomsprop(
+			xselev->display,
+			xselev->requestor,
+			xselev->property,
+			atom_pair,
+			&pairs
+		);
+		if (natoms == 0 || pairs == NULL) {
+			free(pairs);
+			return CTRLSEL_ERROR;
+		}
+	} else {
+		pair[PAIR_TARGET] = xselev->target;
+		pair[PAIR_PROPERTY] = xselev->property;
+		pairs = pair;
+		natoms = 2;
+	}
+	success = 1;
+	for (j = 0; j < natoms; j += 2) {
+		targetp = NULL;
+		for (i = 0; i < context->ntargets; i++) {
+			if (pairs[j + PAIR_TARGET] == context->targets[i].target) {
+				targetp = &context->targets[i];
+				break;
+			}
+		}
+		if (pairs[j + PAIR_PROPERTY] == None)
+			pairs[j + PAIR_PROPERTY] = pairs[j + PAIR_TARGET];
+		if (targetp == NULL) {
+			success = 0;
+			continue;
+		}
+		status = getcontent(
+			targetp,
+			xselev->display,
+			xselev->requestor,
+			pairs[j + PAIR_PROPERTY]
+		);
+		switch (status) {
+		case CONTENT_ERROR:
+			success = 0;
+			break;
+		case CONTENT_SUCCESS:
+			/* fallthrough */
+		case CONTENT_ZERO:
+			context->ndone++;
+			break;
+		case CONTENT_INCR:
+			if (!newtransfer(context, targetp, xselev->requestor, pairs[j + PAIR_PROPERTY]))
+				success = 0;
+			break;
+		}
+	}
+	if (xselev->target == multiple)
+		free(pairs);
+	return success ? CTRLSEL_INTERNAL : CTRLSEL_ERROR;
+}
+
+static int
+receiveincr(CtrlSelContext *context, XEvent *xev)
+{
+	struct Transfer *transfer;
+	XPropertyEvent *xpropev;
+	int status;
+
+	xpropev = &xev->xproperty;
+	if (xpropev->state != PropertyNewValue)
+		return CTRLSEL_NONE;
+	if (xpropev->window != context->window)
+		return CTRLSEL_NONE;
+	for (transfer = (struct Transfer *)context->transfers; transfer != NULL; transfer = transfer->next)
+		if (transfer->property == xpropev->atom)
+			goto found;
+	return CTRLSEL_NONE;
+found:
+	status = getcontent(
+		transfer->target,
+		xpropev->display,
+		xpropev->window,
+		xpropev->atom
+	);
+	switch (status) {
+	case CONTENT_ERROR:
+	case CONTENT_INCR:
+		return CTRLSEL_ERROR;
+	case CONTENT_SUCCESS:
+		return CTRLSEL_INTERNAL;
+	case CONTENT_ZERO:
+		context->ndone++;
+		deltransfer(context, transfer);
+		break;
+	}
+	return CTRLSEL_INTERNAL;
+}
+
+int
+ctrlsel_receive(CtrlSelContext *context, XEvent *xev)
+{
+	int status;
+
+	if (xev->type == SelectionNotify)
+		status = receiveinit(context, xev);
+	else if (xev->type == PropertyNotify)
+		status = receiveincr(context, xev);
+	else
+		return CTRLSEL_NONE;
+	if (status == CTRLSEL_INTERNAL) {
+		if (context->ndone >= context->ntargets) {
+			status = CTRLSEL_RECEIVED;
+			goto done;
+		}
+	} else if (status == CTRLSEL_ERROR) {
+		freebuffers(context);
+		freetransferences(context);
+	}
+done:
+	if (status == CTRLSEL_RECEIVED)
+		freetransferences(context);
+	return status;
+}
+
+static int
+sendinit(CtrlSelContext *context, XEvent *xev)
+{
+	XSelectionRequestEvent *xreqev;
+	XSelectionEvent xselev;
+	unsigned long natoms, i;
+	Atom *pairs;
+	Atom pair[PAIR_LAST];
+	Atom multiple, atom_pair;
+	Bool success;
+
+	xreqev = &xev->xselectionrequest;
+	if (xreqev->selection != context->selection)
+		return CTRLSEL_NONE;
+	multiple = XInternAtom(context->display, MULTIPLE, False);
+	atom_pair = XInternAtom(context->display, ATOM_PAIR, False);
+	xselev = (XSelectionEvent){
+		.type           = SelectionNotify,
+		.display        = xreqev->display,
+		.requestor      = xreqev->requestor,
+		.selection      = xreqev->selection,
+		.time           = xreqev->time,
+		.target         = xreqev->target,
+		.property       = None,
+	};
+	if (xreqev->time != CurrentTime && xreqev->time < context->time) {
+		/*
+		 * According to ICCCM, the selection owner
+		 * should compare the timestamp with the period
+		 * it has owned the selection and, if the time
+		 * is outside, refuse the `SelectionRequest` by
+		 * sending the requestor window a
+		 * `SelectionNotify` event with the property set
+		 * to `None` (by means of a `SendEvent` request
+		 * with an empty event mask).
+		 */
+		goto done;
+	}
+	if (xreqev->target == multiple) {
+		if (xreqev->property == None)
+			goto done;
+		natoms = getatomsprop(
+			xreqev->display,
+			xreqev->requestor,
+			xreqev->property,
+			atom_pair,
+			&pairs
+		);
+	} else {
+		pair[PAIR_TARGET] = xreqev->target;
+		pair[PAIR_PROPERTY] = xreqev->property;
+		pairs = pair;
+		natoms = 2;
+	}
+	success = True;
+	for (i = 0; i < natoms; i += 2) {
+		if (!convert(context, xreqev->requestor,
+		             pairs[i + PAIR_TARGET],
+		             pairs[i + PAIR_PROPERTY])) {
+			success = False;
+			pairs[i + PAIR_PROPERTY] = None;
+		}
+	}
+	if (xreqev->target == multiple) {
+		XChangeProperty(
+			xreqev->display,
+			xreqev->requestor,
+			xreqev->property,
+			atom_pair,
+			32, PropModeReplace,
+			(unsigned char *)pairs,
+			natoms
+		);
+		free(pairs);
+	}
+	if (success) {
+		if (xreqev->property == None) {
+			xselev.property = xreqev->target;
+		} else {
+			xselev.property = xreqev->property;
+		}
+	}
+done:
+	XSendEvent(
+		xreqev->display,
+		xreqev->requestor,
+		False,
+		NoEventMask,
+		(XEvent *)&xselev
+	);
+	return CTRLSEL_INTERNAL;
+}
+
+static int
+sendlost(CtrlSelContext *context, XEvent *xev)
+{
+	XSelectionClearEvent *xclearev;
+
+	xclearev = &xev->xselectionclear;
+	if (xclearev->selection == context->selection &&
+	    xclearev->window == context->window) {
+		return CTRLSEL_LOST;
+	}
+	return CTRLSEL_NONE;
+}
+
+static int
+senddestroy(CtrlSelContext *context, XEvent *xev)
+{
+	struct Transfer *transfer;
+	XDestroyWindowEvent *xdestroyev;
+
+	xdestroyev = &xev->xdestroywindow;
+	for (transfer = context->transfers; transfer != NULL; transfer = transfer->next)
+		if (transfer->requestor == xdestroyev->window)
+			deltransfer(context, transfer);
+	return CTRLSEL_NONE;
+}
+
+static int
+sendincr(CtrlSelContext *context, XEvent *xev)
+{
+	struct Transfer *transfer;
+	XPropertyEvent *xpropev;
+	unsigned long size;
+
+	xpropev = &xev->xproperty;
+	if (xpropev->state != PropertyDelete)
+		return CTRLSEL_NONE;
+	for (transfer = context->transfers; transfer != NULL; transfer = transfer->next)
+		if (transfer->property == xpropev->atom &&
+		    transfer->requestor == xpropev->window)
+			goto found;
+	return CTRLSEL_NONE;
+found:
+	if (transfer->size >= transfer->target->bufsize)
+		transfer->size = transfer->target->bufsize;
+	size = transfer->target->bufsize - transfer->size;
+	if (size > context->selmaxsize)
+		size = context->selmaxsize;
+	XChangeProperty(
+		xpropev->display,
+		xpropev->window,
+		xpropev->atom,
+		transfer->target->target,
+		transfer->target->format,
+		PropModeReplace,
+		transfer->target->buffer + transfer->size,
+		size / nbytes(transfer->target->format)
+	);
+	if (transfer->size >= transfer->target->bufsize) {
+		deltransfer(context, transfer);
+	} else {
+		transfer->size += size;
+	}
+	return CTRLSEL_INTERNAL;
+}
+
+int
+ctrlsel_send(CtrlSelContext *context, XEvent *xev)
+{
+	int status;
+
+	if (xev->type == SelectionRequest)
+		status = sendinit(context, xev);
+	else if (xev->type == SelectionClear)
+		status = sendlost(context, xev);
+	else if (xev->type == DestroyNotify)
+		status = senddestroy(context, xev);
+	else if (xev->type == PropertyNotify)
+		status = sendincr(context, xev);
+	else
+		return CTRLSEL_NONE;
+	if (status == CTRLSEL_LOST || status == CTRLSEL_ERROR) {
+		status = CTRLSEL_LOST;
+		freetransferences(context);
+	}
+	return status;
+}
+
+void
+ctrlsel_cancel(CtrlSelContext *context)
+{
+	if (context == NULL)
+		return;
+	freebuffers(context);
+	freetransferences(context);
+	free(context);
+}
+
+void
+ctrlsel_disown(CtrlSelContext *context)
+{
+	if (context == NULL)
+		return;
+	freetransferences(context);
+	free(context);
+}
+
+static Bool
+dndpred(Display *display, XEvent *event, XPointer p)
+{
+	struct PredArg *arg;
+	struct Transfer *transfer;
+
+	arg = (struct PredArg *)p;
+	switch (event->type) {
+	case KeyPress:
+	case KeyRelease:
+		if (event->xkey.display == display &&
+		    event->xkey.window == arg->window)
+			return True;
+		break;
+	case ButtonPress:
+	case ButtonRelease:
+		if (event->xbutton.display == display &&
+		    event->xbutton.window == arg->window)
+			return True;
+		break;
+	case MotionNotify:
+		if (event->xmotion.display == display &&
+		    event->xmotion.window == arg->window)
+			return True;
+		break;
+	case DestroyNotify:
+		if (event->xdestroywindow.display == display &&
+		    event->xdestroywindow.window == arg->window)
+			return True;
+		break;
+	case UnmapNotify:
+		if (event->xunmap.display == display &&
+		    event->xunmap.window == arg->window)
+			return True;
+		break;
+	case SelectionClear:
+		if (event->xselectionclear.display == display &&
+		    event->xselectionclear.window == arg->window)
+			return True;
+		break;
+	case SelectionRequest:
+		if (event->xselectionrequest.display == display &&
+		    event->xselectionrequest.owner == arg->window)
+			return True;
+		break;
+	case ClientMessage:
+		if (event->xclient.display == display &&
+		    event->xclient.window == arg->window &&
+		    event->xclient.message_type == arg->message_type)
+			return True;
+		break;
+	case PropertyNotify:
+		if (event->xproperty.display != display ||
+		    event->xproperty.state != PropertyDelete)
+			return False;
+		for (transfer = arg->context->transfers;
+		     transfer != NULL;
+		     transfer = transfer->next) {
+			if (transfer->property == event->xproperty.atom &&
+			    transfer->requestor == event->xproperty.window) {
+				return True;
+			}
+		}
+		break;
+	default:
+		break;
+	}
+	return False;
+}
+
+#define SOME(a, b, c)      ((a) != None ? (a) : ((b) != None ? (b) : (c)))
+
+static Cursor
+getcursor(Cursor cursors[CURSOR_LAST], int type)
+{
+	switch (type) {
+	case CURSOR_TARGET:
+	case CURSOR_DRAG:
+		return SOME(cursors[CURSOR_DRAG], cursors[CURSOR_TARGET], None);
+	case CURSOR_PIRATE:
+	case CURSOR_NODROP:
+		return SOME(cursors[CURSOR_NODROP], cursors[CURSOR_PIRATE], None);
+	case CURSOR_COPY:
+		return SOME(cursors[CURSOR_COPY], cursors[CURSOR_DRAG], cursors[CURSOR_TARGET]);
+	case CURSOR_MOVE:
+		return SOME(cursors[CURSOR_MOVE], cursors[CURSOR_DRAG], cursors[CURSOR_TARGET]);
+	case CURSOR_LINK:
+		return SOME(cursors[CURSOR_LINK], cursors[CURSOR_DRAG], cursors[CURSOR_TARGET]);
+	};
+	return None;
+}
+
+static void
+initcursors(Display *display, Cursor cursors[CURSOR_LAST])
+{
+	cursors[CURSOR_TARGET] = XCreateFontCursor(display, XC_target);
+	cursors[CURSOR_PIRATE] = XCreateFontCursor(display, XC_pirate);
+	cursors[CURSOR_DRAG] = XcursorLibraryLoadCursor(display, "dnd-none");
+	cursors[CURSOR_COPY] = XcursorLibraryLoadCursor(display, "dnd-copy");
+	cursors[CURSOR_MOVE] = XcursorLibraryLoadCursor(display, "dnd-move");
+	cursors[CURSOR_LINK] = XcursorLibraryLoadCursor(display, "dnd-link");
+	cursors[CURSOR_NODROP] = XcursorLibraryLoadCursor(display, "forbidden");
+}
+
+static void
+freecursors(Display *display, Cursor cursors[CURSOR_LAST])
+{
+	int i;
+
+	for (i = 0; i < CURSOR_LAST; i++) {
+		if (cursors[i] != None) {
+			XFreeCursor(display, cursors[i]);
+		}
+	}
+}
+
+static int
+querypointer(Display *display, Window window, int *retx, int *rety, Window *retwin)
+{
+	Window root, child;
+	unsigned int mask;
+	int rootx, rooty;
+	int x, y;
+	int retval;
+
+	retval = XQueryPointer(
+		display,
+		window,
+		&root, &child,
+		&rootx, &rooty,
+		&x, &y,
+		&mask
+	);
+	if (retwin != NULL)
+		*retwin = child;
+	if (retx != NULL)
+		*retx = x;
+	if (rety != NULL)
+		*rety = y;
+	return retval;
+}
+
+static Window
+getdndwindowbelow(Display *display, Window root, Atom aware, Atom *version)
+{
+	Atom *p;
+	Window window;
+
+	/*
+	 * Query pointer location and return the window below it,
+	 * and the version of the XDND protocol it uses.
+	 */
+	*version = None;
+	window = root;
+	p = NULL;
+	while (querypointer(display, window, NULL, NULL, &window)) {
+		if (window == None)
+			break;
+		p = NULL;
+		if (getatomsprop(display, window, aware, AnyPropertyType, &p) > 0) {
+			*version = *p;
+			XFree(p);
+			return window;
+		}
+	}
+	XFree(p);
+	return None;
+}
+
+CtrlSelContext *
+ctrlsel_dndwatch(
+	Display *display,
+	Window window,
+	unsigned int actions,
+	struct CtrlSelTarget targets[],
+	unsigned long ntargets
+) {
+	CtrlSelContext *context;
+	Atom version = XDND_VERSION;    /* yes, version is an Atom */
+	Atom xdndaware, xdndselection;
+
+	xdndaware = XInternAtom(display, atomnames[XDND_AWARE], False);
+	if (xdndaware == None)
+		return NULL;
+	xdndselection = XInternAtom(display, atomnames[XDND_SELECTION], False);
+	if (xdndselection == None)
+		return NULL;
+	if ((context = malloc(sizeof(*context))) == NULL)
+		return NULL;
+	*context = (CtrlSelContext){
+		.display = display,
+		.window = window,
+		.selection = xdndselection,
+		.time = CurrentTime,
+		.targets = targets,
+		.ntargets = ntargets,
+		.selmaxsize = getselmaxsize(display),
+		.ndone = 0,
+		.transfers = NULL,
+		.dndwindow = None,
+		.dndactions = actions,
+		.dndresult = 0x00,
+	};
+	(void)XChangeProperty(
+		display,
+		window,
+		xdndaware,
+		XA_ATOM, 32,
+		PropModeReplace,
+		(unsigned char *)&version,
+		1
+	);
+	return context;
+}
+
+static void
+finishdrop(CtrlSelContext *context)
+{
+	long d[NCLIENTMSG_DATA];
+	unsigned long i;
+	Atom finished;
+
+	if (context->dndwindow == None)
+		return;
+	finished = XInternAtom(context->display, atomnames[XDND_FINISHED], False);
+	if (finished == None)
+		return;
+	for (i = 0; i < context->ntargets; i++)
+		context->targets[i].action = context->dndresult;
+	d[0] = context->window;
+	d[1] = d[2] = d[3] = d[4] = 0;
+	clientmsg(context->display, context->dndwindow, finished, d);
+	context->dndwindow = None;
+}
+
+int
+ctrlsel_dndreceive(CtrlSelContext *context, XEvent *event)
+{
+	Atom atoms[XDND_ATOM_LAST];
+	Atom action;
+	long d[NCLIENTMSG_DATA];
+	
+	if (!XInternAtoms(context->display, atomnames, XDND_ATOM_LAST, False, atoms))
+		return CTRLSEL_NONE;
+	switch (ctrlsel_receive(context, event)) {
+	case CTRLSEL_RECEIVED:
+		finishdrop(context);
+		return CTRLSEL_RECEIVED;
+	case CTRLSEL_INTERNAL:
+	case CTRLSEL_ERROR:
+		return CTRLSEL_INTERNAL;
+	default:
+		break;
+	}
+	if (event->type != ClientMessage)
+		return CTRLSEL_NONE;
+	if (event->xclient.message_type == atoms[XDND_ENTER]) {
+		context->dndwindow = (Window)event->xclient.data.l[0];
+		context->dndresult = 0x00;
+	} else if (event->xclient.message_type == atoms[XDND_LEAVE]) {
+		if ((Window)event->xclient.data.l[0] == None ||
+		    (Window)event->xclient.data.l[0] != context->dndwindow)
+			return CTRLSEL_NONE;
+		context->dndwindow = None;
+	} else if (event->xclient.message_type == atoms[XDND_DROP]) {
+		if ((Window)event->xclient.data.l[0] == None ||
+		    (Window)event->xclient.data.l[0] != context->dndwindow)
+			return CTRLSEL_NONE;
+		context->time = (Time)event->xclient.data.l[2];
+		(void)request(context);
+	} else if (event->xclient.message_type == atoms[XDND_POSITION]) {
+		if ((Window)event->xclient.data.l[0] == None ||
+		    (Window)event->xclient.data.l[0] != context->dndwindow)
+			return CTRLSEL_NONE;
+		if (((Atom)event->xclient.data.l[4] == atoms[XDND_ACTION_COPY] &&
+		     context->dndactions & CTRLSEL_COPY) ||
+		    ((Atom)event->xclient.data.l[4] == atoms[XDND_ACTION_MOVE] &&
+		     context->dndactions & CTRLSEL_MOVE) ||
+		    ((Atom)event->xclient.data.l[4] == atoms[XDND_ACTION_LINK] &&
+		     context->dndactions & CTRLSEL_LINK) ||
+		    ((Atom)event->xclient.data.l[4] == atoms[XDND_ACTION_ASK] &&
+		     context->dndactions & CTRLSEL_ASK) ||
+		    ((Atom)event->xclient.data.l[4] == atoms[XDND_ACTION_PRIVATE] &&
+		     context->dndactions & CTRLSEL_PRIVATE)) {
+			action = (Atom)event->xclient.data.l[4];
+		} else {
+			action = atoms[XDND_ACTION_COPY];
+		}
+		d[0] = context->window;
+		d[1] = 0x1;
+		d[2] = 0;               /* our rectangle is the entire screen */
+		d[3] = 0xFFFFFFFF;      /* so we do not get lots of messages */
+		d[4] = action;
+		if (action == atoms[XDND_ACTION_PRIVATE])
+			context->dndresult = CTRLSEL_PRIVATE;
+		else if (action == atoms[XDND_ACTION_ASK])
+			context->dndresult = CTRLSEL_ASK;
+		else if (action == atoms[XDND_ACTION_LINK])
+			context->dndresult = CTRLSEL_LINK;
+		else if (action == atoms[XDND_ACTION_MOVE])
+			context->dndresult = CTRLSEL_MOVE;
+		else
+			context->dndresult = CTRLSEL_COPY;
+		clientmsg(
+			context->display,
+			(Window)event->xclient.data.l[0],
+			atoms[XDND_STATUS],
+			d
+		);
+	} else {
+		return CTRLSEL_NONE;
+	}
+	return CTRLSEL_INTERNAL;
+}
+
+void
+ctrlsel_dndclose(CtrlSelContext *context)
+{
+	if (context == NULL)
+		return;
+	finishdrop(context);
+	freebuffers(context);
+	freetransferences(context);
+	free(context);
+}
+
+void
+ctrlsel_dnddisown(CtrlSelContext *context)
+{
+	ctrlsel_disown(context);
+}
+
+int
+ctrlsel_dndsend(CtrlSelContext *context, XEvent *event)
+{
+	Atom finished;
+
+	finished = XInternAtom(context->display, atomnames[XDND_FINISHED], False);
+	if (event->type == ClientMessage &&
+	    event->xclient.message_type == finished &&
+	    (Window)event->xclient.data.l[0] == context->dndwindow) {
+		ctrlsel_dnddisown(context);
+		return CTRLSEL_SENT;
+	}
+	return ctrlsel_send(context, event);
+}
+
+int
+ctrlsel_dndown(
+	Display *display,
+	Window window,
+	Window miniature,
+	Time time,
+	struct CtrlSelTarget targets[],
+	unsigned long ntargets,
+	CtrlSelContext **context_ret
+) {
+	CtrlSelContext *context;
+	struct PredArg arg;
+	XWindowAttributes wattr;
+	XEvent event;
+	Atom atoms[XDND_ATOM_LAST];
+	Cursor cursors[CURSOR_LAST] = { None, None };
+	Cursor cursor;
+	Window lastwin, winbelow;
+	Atom lastaction, action, version;
+	long d[NCLIENTMSG_DATA];
+	int sendposition, retval, status, inside;
+	int x, y, w, h;
+
+	*context_ret = NULL;
+	if (display == NULL || window == None)
+		return CTRLSEL_ERROR;
+	if (!XGetWindowAttributes(display, window, &wattr))
+		return CTRLSEL_ERROR;
+	if ((wattr.your_event_mask & StructureNotifyMask) == 0x00)
+		return CTRLSEL_ERROR;
+	if (wattr.map_state != IsViewable)
+		return CTRLSEL_ERROR;
+	if (!XInternAtoms(display, atomnames, XDND_ATOM_LAST, False, atoms))
+		return CTRLSEL_ERROR;
+	context = ctrlsel_setowner(
+		display,
+		window,
+		atoms[XDND_SELECTION],
+		time,
+		0,
+		targets,
+		ntargets
+	);
+	if (context == NULL)
+		return CTRLSEL_ERROR;
+	d[0] = window;
+	sendposition = 1;
+	x = y = w = h = 0;
+	retval = CTRLSEL_ERROR;
+	lastaction = action = None;
+	lastwin = None;
+	arg = (struct PredArg){
+		.context = context,
+		.window = window,
+		.message_type = atoms[XDND_STATUS],
+	};
+	initcursors(display, cursors);
+	status = XGrabPointer(
+		display,
+		window,
+		True,
+		ButtonPressMask | ButtonMotionMask |
+		ButtonReleaseMask | PointerMotionMask,
+		GrabModeAsync,
+		GrabModeAsync,
+		None,
+		None,
+		time
+	);
+	if (status != GrabSuccess)
+		goto done;
+	status = XGrabKeyboard(
+		display,
+		window,
+		True,
+		GrabModeAsync,
+		GrabModeAsync,
+		time
+	);
+	if (status != GrabSuccess)
+		goto done;
+	if (miniature != None)
+		XMapRaised(display, miniature);
+	cursor = getcursor(cursors, CURSOR_DRAG);
+	for (;;) {
+		(void)XIfEvent(display, &event, &dndpred, (XPointer)&arg);
+		switch (ctrlsel_send(context, &event)) {
+		case CTRLSEL_LOST:
+			retval = CTRLSEL_NONE;
+			goto done;
+		case CTRLSEL_INTERNAL:
+			continue;
+		default:
+			break;
+		}
+		switch (event.type) {
+		case KeyPress:
+		case KeyRelease:
+			if (event.xkey.keycode != 0 &&
+			    event.xkey.keycode == XKeysymToKeycode(display, XK_Escape)) {
+				retval = CTRLSEL_NONE;
+				goto done;
+			}
+			break;
+		case ButtonPress:
+		case ButtonRelease:
+			if (lastwin == None) {
+				retval = CTRLSEL_NONE;
+			} else if (lastwin == window) {
+				retval = CTRLSEL_DROPSELF;
+			} else {
+				retval = CTRLSEL_DROPOTHER;
+				d[1] = d[3] = d[4] = 0;
+				d[2] = event.xbutton.time;
+				clientmsg(display, lastwin, atoms[XDND_DROP], d);
+				context->dndwindow = lastwin;
+			}
+			goto done;
+		case MotionNotify:
+			if (event.xmotion.time - time < MOTION_TIME)
+				break;
+			if (miniature != None) {
+				XMoveWindow(
+					display,
+					miniature,
+					event.xmotion.x_root + DND_DISTANCE,
+					event.xmotion.y_root + DND_DISTANCE
+				);
+			}
+			inside = between(event.xmotion.x, event.xmotion.y, x, y, w, h);
+			if ((lastaction != action || sendposition || !inside)
+			    && lastwin != None) {
+				if (lastaction != None)
+					d[4] = lastaction;
+				else if (FLAG(event.xmotion.state, ControlMask|ShiftMask))
+					d[4] = atoms[XDND_ACTION_LINK];
+				else if (FLAG(event.xmotion.state, ShiftMask))
+					d[4] = atoms[XDND_ACTION_MOVE];
+				else if (FLAG(event.xmotion.state, ControlMask))
+					d[4] = atoms[XDND_ACTION_COPY];
+				else
+					d[4] = atoms[XDND_ACTION_ASK];
+				d[1] = 0;
+				d[2] = event.xmotion.x_root << 16;
+				d[2] |= event.xmotion.y_root & 0xFFFF;
+				d[3] = event.xmotion.time;
+				clientmsg(display, lastwin, atoms[XDND_POSITION], d);
+				sendposition = 1;
+			}
+			time = event.xmotion.time;
+			lastaction = action;
+			winbelow = getdndwindowbelow(display, wattr.root, atoms[XDND_AWARE], &version);
+			if (winbelow == lastwin)
+				break;
+			sendposition = 1;
+			x = y = w = h = 0;
+			if (version > XDND_VERSION)
+				version = XDND_VERSION;
+			if (lastwin != None && lastwin != window) {
+				d[1] = d[2] = d[3] = d[4] = 0;
+				clientmsg(display, lastwin, atoms[XDND_LEAVE], d);
+			}
+			if (winbelow != None && winbelow != window) {
+				d[1] = version;
+				d[1] <<= 24;
+				d[2] = ntargets > 0 ? targets[0].target : None;
+				d[3] = ntargets > 1 ? targets[1].target : None;
+				d[4] = ntargets > 2 ? targets[2].target : None;
+				clientmsg(display, winbelow, atoms[XDND_ENTER], d);
+			}
+			if (winbelow == None)
+				cursor = getcursor(cursors, CURSOR_NODROP);
+			else if (FLAG(event.xmotion.state, ControlMask|ShiftMask))
+				cursor = getcursor(cursors, CURSOR_LINK);
+			else if (FLAG(event.xmotion.state, ShiftMask))
+				cursor = getcursor(cursors, CURSOR_MOVE);
+			else if (FLAG(event.xmotion.state, ControlMask))
+				cursor = getcursor(cursors, CURSOR_COPY);
+			else
+				cursor = getcursor(cursors, CURSOR_DRAG);
+			XDefineCursor(display, window, cursor);
+			lastwin = winbelow;
+			lastaction = action = None;
+			break;
+		case ClientMessage:
+			if ((Window)event.xclient.data.l[0] != lastwin)
+				break;
+			sendposition = (event.xclient.data.l[1] & 0x02);
+			if (event.xclient.data.l[1] & 0x01)
+				XDefineCursor(display, window, cursor);
+			else
+				XDefineCursor(display, window, getcursor(cursors, CURSOR_NODROP));
+			x = event.xclient.data.l[2] >> 16;
+			y = event.xclient.data.l[2] & 0xFFF;
+			w = event.xclient.data.l[3] >> 16;
+			h = event.xclient.data.l[3] & 0xFFF;
+			if ((Atom)event.xclient.data.l[4] != None)
+				action = (Atom)event.xclient.data.l[4];
+			else
+				action = atoms[XDND_ACTION_COPY];
+			break;
+		case DestroyNotify:
+		case UnmapNotify:
+			XPutBackEvent(display, &event);
+			retval = CTRLSEL_ERROR;
+			goto done;
+		default:
+			break;
+		}
+	}
+done:
+	XUndefineCursor(display, window);
+	if (miniature != None)
+		XUnmapWindow(display, miniature);
+	XUngrabPointer(display, CurrentTime);
+	XUngrabKeyboard(display, CurrentTime);
+	freecursors(display, cursors);
+	if (retval != CTRLSEL_DROPOTHER) {
+		ctrlsel_dnddisown(context);
+		context = NULL;
+	}
+	*context_ret = context;
+	return retval;
+}
diff --git a/src/ctrlsel.h b/src/ctrlsel.h
@@ -0,0 +1,131 @@
+/*
+ * MIT/X Consortium License
+ *
+ * © 2022-2023 Lucas de Sena <lucas at seninha dot org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * ctrlsel: X11 selection ownership and request helper functions
+ *
+ * Refer to the accompanying manual for a description of the interface.
+ */
+#ifndef _CTRLSEL_H_
+#define _CTRLSEL_H_
+
+enum {
+	CTRLSEL_NONE,
+	CTRLSEL_INTERNAL,
+	CTRLSEL_RECEIVED,
+	CTRLSEL_SENT,
+	CTRLSEL_DROPSELF,
+	CTRLSEL_DROPOTHER,
+	CTRLSEL_ERROR,
+	CTRLSEL_LOST
+};
+
+enum {
+	CTRLSEL_COPY    = 0x01,
+	CTRLSEL_MOVE    = 0x02,
+	CTRLSEL_LINK    = 0x04,
+	CTRLSEL_ASK     = 0x08,
+	CTRLSEL_PRIVATE = 0x10,
+};
+
+typedef struct CtrlSelContext CtrlSelContext;
+
+struct CtrlSelTarget {
+	Atom target;
+	Atom type;
+	int format;
+	unsigned int action;
+	unsigned long nitems;
+	unsigned long bufsize;
+	unsigned char *buffer;
+};
+
+void
+ctrlsel_filltarget(
+	Atom target,
+	Atom type,
+	int format,
+	unsigned char *buffer,
+	unsigned long size,
+	struct CtrlSelTarget *fill
+);
+
+CtrlSelContext *
+ctrlsel_request(
+	Display *display,
+	Window window,
+	Atom selection,
+	Time time,
+	struct CtrlSelTarget targets[],
+	unsigned long ntargets
+);
+
+CtrlSelContext *
+ctrlsel_setowner(
+	Display *display,
+	Window window,
+	Atom selection,
+	Time time,
+	int ismanager,
+	struct CtrlSelTarget targets[],
+	unsigned long ntargets
+);
+
+int ctrlsel_receive(struct CtrlSelContext *context, XEvent *event);
+
+int ctrlsel_send(struct CtrlSelContext *context, XEvent *event);
+
+void ctrlsel_cancel(struct CtrlSelContext *context);
+
+void ctrlsel_disown(struct CtrlSelContext *context);
+
+CtrlSelContext *
+ctrlsel_dndwatch(
+	Display *display,
+	Window window,
+	unsigned int actions,
+	struct CtrlSelTarget targets[],
+	unsigned long ntargets
+);
+
+int ctrlsel_dndreceive(struct CtrlSelContext *context, XEvent *event);
+
+void ctrlsel_dndclose(struct CtrlSelContext *context);
+
+int
+ctrlsel_dndown(
+	Display *display,
+	Window window,
+	Window miniature,
+	Time time,
+	struct CtrlSelTarget targets[],
+	unsigned long ntargets,
+	CtrlSelContext **context
+);
+
+int ctrlsel_dndsend(struct CtrlSelContext *context, XEvent *event);
+
+void ctrlsel_dnddisown(struct CtrlSelContext *context);
+
+#endif /* _CTRLSEL_H_ */
diff --git a/src/entry.c b/src/entry.c
@@ -67,11 +67,16 @@ static void cursor_left(ltk_entry *entry, ltk_key_event *event);
 static void cursor_right(ltk_entry *entry, ltk_key_event *event);
 static void expand_selection_left(ltk_entry *entry, ltk_key_event *event);
 static void expand_selection_right(ltk_entry *entry, ltk_key_event *event);
+static void selection_to_primary(ltk_entry *entry, ltk_key_event *event);
+static void selection_to_clipboard(ltk_entry *entry, ltk_key_event *event);
+static void paste_primary(ltk_entry *entry, ltk_key_event *event);
+static void paste_clipboard(ltk_entry *entry, ltk_key_event *event);
 static void select_all(ltk_entry *entry, ltk_key_event *event);
 static void delete_char_backwards(ltk_entry *entry, ltk_key_event *event);
 static void delete_char_forwards(ltk_entry *entry, ltk_key_event *event);
 static void recalc_ideal_size(ltk_entry *entry);
 static void ensure_cursor_shown(ltk_entry *entry);
+static void insert_text(ltk_entry *entry, char *text, size_t len);
 
 struct key_cb {
 	char *text;
@@ -87,7 +92,11 @@ static struct key_cb cb_map[] = {
 	{"delete-char-forwards", &delete_char_forwards},
 	{"expand-selection-left", &expand_selection_left},
 	{"expand-selection-right", &expand_selection_right},
+	{"paste-clipboard", &paste_clipboard},
+	{"paste-primary", &paste_primary},
 	{"select-all", &select_all},
+	{"selection-to-clipboard", &selection_to_clipboard},
+	{"selection-to-primary", &selection_to_primary},
 };
 
 struct keypress_cfg {
@@ -360,6 +369,46 @@ expand_selection(ltk_entry *entry, int dir) {
 		entry->pos = new;
 		wipe_selection(entry);
 	}
+	selection_to_primary(entry, NULL);
+}
+
+/* FIXME: different programs have different behaviors when they set the selection */
+static void
+selection_to_primary(ltk_entry *entry, ltk_key_event *event) {
+	(void)event;
+	if (entry->sel_end == entry->sel_start)
+		return;
+	txtbuf *primary = ltk_clipboard_get_primary_buffer(entry->widget.window->clipboard);
+	txtbuf_clear(primary);
+	txtbuf_appendn(primary, entry->text + entry->sel_start, entry->sel_end - entry->sel_start);
+	ltk_clipboard_set_primary_selection_owner(entry->widget.window->clipboard);
+}
+
+static void
+selection_to_clipboard(ltk_entry *entry, ltk_key_event *event) {
+	(void)event;
+	if (entry->sel_end == entry->sel_start)
+		return;
+	txtbuf *clip = ltk_clipboard_get_clipboard_buffer(entry->widget.window->clipboard);
+	txtbuf_clear(clip);
+	txtbuf_appendn(clip, entry->text + entry->sel_start, entry->sel_end - entry->sel_start);
+	ltk_clipboard_set_clipboard_selection_owner(entry->widget.window->clipboard);
+}
+
+static void
+paste_primary(ltk_entry *entry, ltk_key_event *event) {
+	(void)event;
+	txtbuf *buf = ltk_clipboard_get_primary_text(entry->widget.window->clipboard);
+	if (buf)
+		insert_text(entry, buf->text, buf->len);
+}
+
+static void
+paste_clipboard(ltk_entry *entry, ltk_key_event *event) {
+	(void)event;
+	txtbuf *buf = ltk_clipboard_get_clipboard_text(entry->widget.window->clipboard);
+	if (buf)
+		insert_text(entry, buf->text, buf->len);
 }
 
 static void
@@ -414,6 +463,8 @@ static void
 select_all(ltk_entry *entry, ltk_key_event *event) {
 	(void)event;
 	set_selection(entry, 0, entry->len);
+	if (entry->len)
+		selection_to_primary(entry, NULL);
 	entry->sel_side = 0;
 }
 
@@ -454,8 +505,16 @@ ensure_cursor_shown(ltk_entry *entry) {
 /* FIXME: maybe make this a regular key binding with wildcard text like in ledit? */
 static void
 insert_text(ltk_entry *entry, char *text, size_t len) {
-	/* FIXME: ignore newlines, etc. */
-	size_t new_alloc = ideal_array_size(entry->alloc, entry->len + len + 1 - (entry->sel_end - entry->sel_start));
+	size_t num = 0;
+	/* FIXME: this is ugly and there are probably a lot of other
+	   cases that need to be handled */
+	/* FIXME: Just ignoring newlines is weird, but what other option is there? */
+	for (size_t i = 0; i < len; i++) {
+		if (text[i] == '\n' || text[i] == '\r')
+			num++;
+	}
+	size_t reallen = len - num;
+	size_t new_alloc = ideal_array_size(entry->alloc, entry->len + reallen + 1 - (entry->sel_end - entry->sel_start));
 	if (new_alloc != entry->alloc) {
 		entry->text = ltk_realloc(entry->text, new_alloc);
 		entry->alloc = new_alloc;
@@ -463,15 +522,18 @@ insert_text(ltk_entry *entry, char *text, size_t len) {
 	/* FIXME: also need to reset selecting status once mouse selections are supported */
 	if (entry->sel_start != entry->sel_end) {
 		entry->pos = entry->sel_start;
-		memmove(entry->text + entry->pos + len, entry->text + entry->sel_end, entry->len - entry->sel_end);
-		entry->len = entry->len + len - (entry->sel_end - entry->sel_start);
+		memmove(entry->text + entry->pos + reallen, entry->text + entry->sel_end, entry->len - entry->sel_end);
+		entry->len = entry->len + reallen - (entry->sel_end - entry->sel_start);
 		wipe_selection(entry);
 	} else {
-		memmove(entry->text + entry->pos + len, entry->text + entry->pos, entry->len - entry->pos);
-		entry->len += len;
+		memmove(entry->text + entry->pos + reallen, entry->text + entry->pos, entry->len - entry->pos);
+		entry->len += reallen;
+	}
+	for (size_t i = 0, j = entry->pos; i < len; i++) {
+		if (text[i] != '\n' && text[i] != '\r')
+			entry->text[j++] = text[i];
 	}
-	memmove(entry->text + entry->pos, text, len);
-	entry->pos += len;
+	entry->pos += reallen;
 	entry->text[entry->len] = '\0';
 	ltk_text_line_set_text(entry->tl, entry->text, 0);
 	recalc_ideal_size(entry);
@@ -489,7 +551,7 @@ ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) {
 		/* FIXME: change naming (rawtext, text, mapped...) */
 		/* FIXME: a bit weird to mask out shift, but if that isn't done, it
 		   would need to be included for all mappings with capital letters */
-		if ((b.mods == event->modmask && b.sym == event->sym) ||
+		if ((b.mods == event->modmask && b.sym != LTK_KEY_NONE && b.sym == event->sym) ||
 		    (b.mods == (event->modmask & ~LTK_MOD_SHIFT) &&
 		     ((b.text && event->mapped && !strcmp(b.text, event->mapped)) ||
 		      (b.rawtext && event->text && !strcmp(b.rawtext, event->text))))) {
@@ -499,7 +561,7 @@ ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) {
 			return 1;
 		}
 	}
-	if (event->text) {
+	if (event->text && (event->modmask & (LTK_MOD_CTRL | LTK_MOD_ALT | LTK_MOD_SUPER)) == 0) {
 		/* FIXME: properly handle everything */
 		if (event->text[0] == '\n' || event->text[0] == '\r' || event->text[0] == 0x1b)
 			return 0;
diff --git a/src/entry.h b/src/entry.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 lumidify <nobody@lumidify.org>
+ * Copyright (c) 2022-2023 lumidify <nobody@lumidify.org>
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
diff --git a/src/event.h b/src/event.h
@@ -58,10 +58,11 @@ typedef union {
 } ltk_event;
 
 #include "ltk.h"
+#include "clipboard.h"
 
 void ltk_events_cleanup(void);
 /* WARNING: Text returned in key and keyboard events must be copied before calling this function again! */
-int ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event);
+int ltk_next_event(ltk_renderdata *renderdata, ltk_clipboard *clip, size_t lang_index, ltk_event *event);
 void ltk_generate_keyboard_event(ltk_renderdata *renderdata, ltk_event *event);
 
 #endif /* LTK_EVENT_H */
diff --git a/src/event_xlib.c b/src/event_xlib.c
@@ -8,6 +8,7 @@
 #include "graphics.h"
 #include "xlib_shared.h"
 #include "config.h"
+#include "clipboard_xlib.h"
 
 #define TEXT_INITIAL_SIZE 128
 
@@ -158,7 +159,7 @@ ltk_generate_keyboard_event(ltk_renderdata *renderdata, ltk_event *event) {
    1 means no events pending,
    2 means event discarded (need to call again) */
 static int
-next_event_base(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) {
+next_event_base(ltk_renderdata *renderdata, ltk_clipboard *clip, size_t lang_index, ltk_event *event) {
 	if (next_event_valid) {
 		next_event_valid = 0;
 		*event = (ltk_event){.button = next_event};
@@ -175,6 +176,8 @@ next_event_base(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event)
 	*event = (ltk_event){.type = LTK_UNKNOWN_EVENT};
 	if (XFilterEvent(&xevent, None))
 		return 2;
+	if (clip && ltk_clipboard_filter_event(clip, &xevent))
+		return 2;
 	int button = 0;
 	switch (xevent.type) {
 	case ButtonPress:
@@ -335,9 +338,9 @@ next_event_base(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event)
 }
 
 int
-ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) {
+ltk_next_event(ltk_renderdata *renderdata, ltk_clipboard *clip, size_t lang_index, ltk_event *event) {
 	int ret = 0;
-	while ((ret = next_event_base(renderdata, lang_index, event)) == 2) {
+	while ((ret = next_event_base(renderdata, clip, lang_index, event)) == 2) {
 		/* NOP */
 	}
 	return ret;
diff --git a/src/ltk.h b/src/ltk.h
@@ -45,12 +45,14 @@ typedef struct ltk_window_theme ltk_window_theme;
 
 #include "widget.h"
 #include "surface_cache.h"
+#include "clipboard.h"
 #include "event.h"
 
 struct ltk_window {
 	ltk_renderdata *renderdata;
 	ltk_surface_cache *surface_cache;
 	ltk_text_context *text_context;
+	ltk_clipboard *clipboard;
 	ltk_surface *surface;
 	ltk_widget *root_widget;
 	ltk_widget *hover_widget;
diff --git a/src/ltkd.c b/src/ltkd.c
@@ -412,6 +412,7 @@ ltk_mainloop(ltk_window *window) {
 	int clifd;
 	struct timeval tv, tv_master;
 	tv_master.tv_sec = 0;
+	/* FIXME: configure this number */
 	tv_master.tv_usec = 20000;
 
 	FD_ZERO(&sock_state.rallfds);
@@ -450,7 +451,7 @@ ltk_mainloop(ltk_window *window) {
 		/* value of tv doesn't really matter anymore here because the
 		   necessary framerate-limiting delay is already done */
 		wretval = select(sock_state.maxfd + 1, NULL, &wfds, NULL, &tv);
-		while (!ltk_next_event(window->renderdata, window->cur_kbd, &event))
+		while (!ltk_next_event(window->renderdata, window->clipboard, window->cur_kbd, &event))
 			ltk_handle_event(window, &event);
 
 		if (rretval > 0 || (sock_write_available && wretval > 0)) {
@@ -1040,7 +1041,6 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
 	window->popups_locked = 0;
 	window->cur_kbd = 0;
 
-	ltk_renderdata *renderer_create_window(const char *title, int x, int y, unsigned int w, unsigned int h);
 	window->renderdata = renderer_create_window(title, x, y, w, h);
 	/* FIXME: search different directories for config */
 	char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg");
@@ -1085,12 +1085,14 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
 	window->surface = ltk_surface_from_window(window->renderdata, w, h);
 
 	window->text_context = ltk_text_context_create(window->renderdata, window->theme->font);
+	window->clipboard = ltk_clipboard_create(window->renderdata);
 
 	return window;
 }
 
 static void
 ltk_destroy_window(ltk_window *window) {
+	ltk_clipboard_destroy(window->clipboard);
 	ltk_text_context_destroy(window->text_context);
 	if (window->popups)
 		ltk_free(window->popups);
diff --git a/src/txtbuf.c b/src/txtbuf.c
@@ -0,0 +1,145 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+
+#include "util.h"
+#include "memory.h"
+#include "txtbuf.h"
+#include "assert.h"
+
+txtbuf *
+txtbuf_new(void) {
+	txtbuf *buf = ltk_malloc(sizeof(txtbuf));
+	buf->text = NULL;
+	buf->cap = buf->len = 0;
+	return buf;
+}
+
+txtbuf *
+txtbuf_new_from_char(char *str) {
+	txtbuf *buf = ltk_malloc(sizeof(txtbuf));
+	buf->text = ltk_strdup(str);
+	buf->len = strlen(str);
+	buf->cap = buf->len + 1;
+	return buf;
+}
+
+txtbuf *
+txtbuf_new_from_char_len(char *str, size_t len) {
+	txtbuf *buf = ltk_malloc(sizeof(txtbuf));
+	buf->text = ltk_strndup(str, len);
+	buf->len = len;
+	buf->cap = len + 1;
+	return buf;
+}
+
+void
+txtbuf_fmt(txtbuf *buf, char *fmt, ...) {
+	va_list args;
+	va_start(args, fmt);
+	int len = vsnprintf(buf->text, buf->cap, fmt, args);
+	/* FIXME: len can never be negative, right? */
+	/* FIXME: maybe also shrink here */
+	if ((size_t)len >= buf->cap) {
+		va_end(args);
+		va_start(args, fmt);
+		txtbuf_resize(buf, len);
+		vsnprintf(buf->text, buf->cap, fmt, args);
+	}
+	buf->len = len;
+	va_end(args);
+}
+
+void
+txtbuf_set_text(txtbuf *buf, char *text) {
+	txtbuf_set_textn(buf, text, strlen(text));
+}
+
+void
+txtbuf_set_textn(txtbuf *buf, char *text, size_t len) {
+	txtbuf_resize(buf, len);
+	buf->len = len;
+	memmove(buf->text, text, len);
+	buf->text[buf->len] = '\0';
+}
+
+void
+txtbuf_append(txtbuf *buf, char *text) {
+	txtbuf_appendn(buf, text, strlen(text));
+}
+
+/* FIXME: some sort of append that does not resize until there's not enough
+   space so a buffer that will be filled up anyways doesn't have to be
+   constantly resized */
+void
+txtbuf_appendn(txtbuf *buf, char *text, size_t len) {
+	/* FIXME: overflow protection here and everywhere else */
+	txtbuf_resize(buf, buf->len + len);
+	memmove(buf->text + buf->len, text, len);
+	buf->len += len;
+	buf->text[buf->len] = '\0';
+}
+
+void
+txtbuf_resize(txtbuf *buf, size_t sz) {
+	/* always leave room for extra \0 */
+	size_t cap = ideal_array_size(buf->cap, sz + 1);
+	if (cap != buf->cap) {
+		buf->text = ltk_realloc(buf->text, cap);
+		buf->cap = cap;
+	}
+}
+
+void
+txtbuf_destroy(txtbuf *buf) {
+	if (!buf)
+		return;
+	free(buf->text);
+	free(buf);
+}
+
+void
+txtbuf_copy(txtbuf *dst, txtbuf *src) {
+	txtbuf_resize(dst, src->len);
+	if (src->text && dst->text) {
+		memcpy(dst->text, src->text, src->len);
+		dst->text[src->len] = '\0';
+	}
+	dst->len = src->len;
+}
+
+txtbuf *
+txtbuf_dup(txtbuf *src) {
+	txtbuf *dst = txtbuf_new();
+	txtbuf_copy(dst, src);
+	return dst;
+}
+
+/* FIXME: proper "normalize" function to add nul-termination if needed */
+int
+txtbuf_cmp(txtbuf *buf1, txtbuf *buf2) {
+	/* FIXME: I guess strcmp would be possible as well since it's nul-terminated now */
+	/* FIXME: Test this because I was tired while writing it */
+	int cmp = strncmp(buf1->text, buf2->text, buf1->len < buf2->len ? buf1->len : buf2->len);
+	if (cmp == 0) {
+		if (buf1->len < buf2->len)
+			return -1;
+		else if (buf1->len > buf2->len)
+			return 1;
+	}
+	return cmp;
+}
+
+int
+txtbuf_eql(txtbuf *buf1, txtbuf *buf2) {
+	return txtbuf_cmp(buf1, buf2) == 0;
+}
+
+void
+txtbuf_clear(txtbuf *buf) {
+	if (buf->len > 0) {
+		buf->len = 0;
+		buf->text[0] = '\0';
+	}
+}
diff --git a/src/txtbuf.h b/src/txtbuf.h
@@ -0,0 +1,99 @@
+#ifndef LTK_TXTBUF_H
+#define LTK_TXTBUF_H
+
+#include <stddef.h>
+
+/*
+ * txtbuf is really just a string data type that is badly named.
+ * The stored text is always nul-terminated.
+ * FIXME: this data type is abused in some places and manually
+ * created so it isn't nul-terminated
+ */
+
+typedef struct {
+	size_t len, cap;
+	char *text;
+} txtbuf;
+
+/*
+ * Create an empty txtbuf.
+ */
+txtbuf *txtbuf_new(void);
+
+/*
+ * Create a new txtbuf, initializing it with the nul-terminated
+ * string 'str'. The input string is copied.
+ */
+txtbuf *txtbuf_new_from_char(char *str);
+
+/*
+ * Create a new txtbuf, initializing it with the string 'str'
+ * of length 'len'. The input string is copied.
+ */
+txtbuf *txtbuf_new_from_char_len(char *str, size_t len);
+
+/*
+ * Replace the stored text in 'buf' with the text generated by
+ * 'snprintf' when called with the given format string and args.
+ */
+void txtbuf_fmt(txtbuf *buf, char *fmt, ...);
+
+/*
+ * Replace the stored text in 'buf' with 'text'.
+ */
+void txtbuf_set_text(txtbuf *buf, char *text);
+
+/*
+ * Same as txtbuf_set_text, but with explicit length for 'text'.
+ */
+void txtbuf_set_textn(txtbuf *buf, char *text, size_t len);
+
+/*
+ * Append 'text' to the text stored in 'buf'.
+ */
+void txtbuf_append(txtbuf *buf, char *text);
+
+/*
+ * Same as txtbuf_append, but with explicit length for 'text'.
+ */
+void txtbuf_appendn(txtbuf *buf, char *text, size_t len);
+
+/*
+ * Compare the text of two txtbuf's like 'strcmp'.
+ */
+int txtbuf_cmp(txtbuf *buf1, txtbuf *buf2);
+
+/*
+ * Convenience function for calling 'txtbuf_cmp' and checking if the
+ * return value is 0, i.e. the strings are equal.
+ */
+int txtbuf_eql(txtbuf *buf1, txtbuf *buf2);
+
+/*
+ * Make sure the txtbuf has space for at least the given size,
+ * plus '\0' at the end.
+ */
+void txtbuf_resize(txtbuf *buf, size_t sz);
+
+/*
+ * Destroy a txtbuf.
+ */
+void txtbuf_destroy(txtbuf *buf);
+
+/*
+ * Copy txtbuf 'src' to txtbuf 'dst'.
+ */
+void txtbuf_copy(txtbuf *dst, txtbuf *src);
+
+/*
+ * Duplicate txtbuf 'src'.
+ */
+txtbuf *txtbuf_dup(txtbuf *src);
+
+/*
+ * Clear the text, but do not reduce the internal capacity
+ * (for efficiency if it will be filled up again anyways).
+ */
+void txtbuf_clear(txtbuf *buf);
+
+#endif /* LTK_TXTBUF */