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