URI: 
       tAdd clipboard support to text entry - ltk - Socket-based GUI for X11 (WIP)
  HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 5bcc196ebfd966a0d6479164d02e4a05d10b38fa
   DIR parent 004ac7555f2e18a6f439e80a8aca23e52f838621
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Mon, 21 Aug 2023 20:25:53 +0200
       
       Add clipboard support to text entry
       
       Diffstat:
         M .ltk/ltk.cfg                        |       2 ++
         M LICENSE                             |       6 +++---
         M Makefile                            |      13 ++++++++++---
         A src/clipboard.h                     |      26 ++++++++++++++++++++++++++
         A src/clipboard_xlib.c                |     252 +++++++++++++++++++++++++++++++
         A src/clipboard_xlib.h                |      11 +++++++++++
         A src/ctrlsel.c                       |    1645 +++++++++++++++++++++++++++++++
         A src/ctrlsel.h                       |     131 +++++++++++++++++++++++++++++++
         M src/entry.c                         |      82 +++++++++++++++++++++++++++----
         M src/entry.h                         |       2 +-
         M src/event.h                         |       3 ++-
         M src/event_xlib.c                    |       9 ++++++---
         M src/ltk.h                           |       2 ++
         M src/ltkd.c                          |       6 ++++--
         A src/txtbuf.c                        |     145 +++++++++++++++++++++++++++++++
         A src/txtbuf.h                        |      99 +++++++++++++++++++++++++++++++
       
       16 files changed, 2411 insertions(+), 23 deletions(-)
       ---
   DIR diff --git a/.ltk/ltk.cfg b/.ltk/ltk.cfg
       t@@ -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]
   DIR diff --git a/LICENSE b/LICENSE
       t@@ -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
   DIR diff --git a/Makefile b/Makefile
       t@@ -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 \
       t@@ -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
       t@@ -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
        
   DIR diff --git a/src/clipboard.h b/src/clipboard.h
       t@@ -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 */
   DIR diff --git a/src/clipboard_xlib.c b/src/clipboard_xlib.c
       t@@ -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);
       +        }
       +}
   DIR diff --git a/src/clipboard_xlib.h b/src/clipboard_xlib.h
       t@@ -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 */
   DIR diff --git a/src/ctrlsel.c b/src/ctrlsel.c
       t@@ -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;
       +}
   DIR diff --git a/src/ctrlsel.h b/src/ctrlsel.h
       t@@ -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_ */
   DIR diff --git a/src/entry.c b/src/entry.c
       t@@ -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;
       t@@ -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 {
       t@@ -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
       t@@ -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;
        }
        
       t@@ -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;
       t@@ -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);
       t@@ -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))))) {
       t@@ -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;
   DIR diff --git a/src/entry.h b/src/entry.h
       t@@ -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
   DIR diff --git a/src/event.h b/src/event.h
       t@@ -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 */
   DIR diff --git a/src/event_xlib.c b/src/event_xlib.c
       t@@ -8,6 +8,7 @@
        #include "graphics.h"
        #include "xlib_shared.h"
        #include "config.h"
       +#include "clipboard_xlib.h"
        
        #define TEXT_INITIAL_SIZE 128
        
       t@@ -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};
       t@@ -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:
       t@@ -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;
   DIR diff --git a/src/ltk.h b/src/ltk.h
       t@@ -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;
   DIR diff --git a/src/ltkd.c b/src/ltkd.c
       t@@ -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);
       t@@ -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)) {
       t@@ -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");
       t@@ -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);
   DIR diff --git a/src/txtbuf.c b/src/txtbuf.c
       t@@ -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';
       +        }
       +}
   DIR diff --git a/src/txtbuf.h b/src/txtbuf.h
       t@@ -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 */