commit 25deb9532df29d093281a2b542be2463809d7379
parent 413b7e3a74968e128ede783a25145dc5aea70c99
Author: lumidify <nobody@lumidify.org>
Date: Mon, 17 May 2021 22:59:57 +0200
Add basic (buggy) clipboard support
Diffstat:
M | common.h | | | 1 | + |
M | ledit.c | | | 292 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
2 files changed, 288 insertions(+), 5 deletions(-)
diff --git a/common.h b/common.h
@@ -28,5 +28,6 @@ typedef struct {
XftColor fg;
XftColor bg;
XftColor scroll_bg;
+ XSetWindowAttributes wattrs;
Atom wm_delete_msg;
} ledit_common_state;
diff --git a/ledit.c b/ledit.c
@@ -2,6 +2,7 @@
/* FIXME: Fix cursor movement, especially buffer->trailing and writing at end of line */
/* FIXME: horizontal scrolling (also need cache to avoid too large pixmaps) */
/* FIXME: sort out types for indices (currently just int, but that might overflow) */
+/* TODO: allow extending selection with shift+mouse like in e.g. gtk */
#include <math.h>
#include <stdio.h>
#include <errno.h>
@@ -11,6 +12,7 @@
#include <unistd.h>
#include <locale.h>
#include <X11/Xlib.h>
+#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/XF86keysym.h>
@@ -123,6 +125,208 @@ static void get_new_line_softline(
int *new_line_ret, int *new_softline_ret
);
+/* clipboard handling largely stolen from st (simple terminal) */
+
+#define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit)))
+
+struct {
+ Atom xtarget;
+ char *primary;
+ size_t primary_alloc;
+ char *clipboard;
+} xsel;
+
+void
+clipcopy(void)
+{
+ Atom clipboard;
+
+ free(xsel.clipboard);
+ xsel.clipboard = NULL;
+
+ if (xsel.primary != NULL) {
+ xsel.clipboard = ledit_strdup(xsel.primary);
+ clipboard = XInternAtom(state.dpy, "CLIPBOARD", 0);
+ XSetSelectionOwner(state.dpy, clipboard, state.win, CurrentTime);
+ }
+}
+
+void
+clippaste(void)
+{
+ Atom clipboard;
+
+ clipboard = XInternAtom(state.dpy, "CLIPBOARD", 0);
+ XConvertSelection(state.dpy, clipboard, xsel.xtarget, clipboard,
+ state.win, CurrentTime);
+}
+
+void
+selpaste(void)
+{
+ XConvertSelection(state.dpy, XA_PRIMARY, xsel.xtarget, XA_PRIMARY,
+ state.win, CurrentTime);
+}
+
+void selnotify(XEvent *e);
+
+void
+propnotify(XEvent *e)
+{
+ XPropertyEvent *xpev;
+ Atom clipboard = XInternAtom(state.dpy, "CLIPBOARD", 0);
+
+ xpev = &e->xproperty;
+ if (xpev->state == PropertyNewValue &&
+ (xpev->atom == XA_PRIMARY ||
+ xpev->atom == clipboard)) {
+ selnotify(e);
+ }
+}
+
+void
+selnotify(XEvent *e)
+{
+ unsigned long nitems, ofs, rem;
+ int format;
+ unsigned char *data, *last, *repl;
+ Atom type, incratom, property = None;
+
+ incratom = XInternAtom(state.dpy, "INCR", 0);
+
+ ofs = 0;
+ if (e->type == SelectionNotify) {
+ property = e->xselection.property;
+ } else if (e->type == PropertyNotify) {
+ property = e->xproperty.atom;
+ }
+
+ if (property == None)
+ return;
+
+ do {
+ if (XGetWindowProperty(state.dpy, state.win, property, ofs,
+ BUFSIZ/4, False, AnyPropertyType,
+ &type, &format, &nitems, &rem,
+ &data)) {
+ fprintf(stderr, "Clipboard allocation failed\n");
+ return;
+ }
+
+ if (e->type == PropertyNotify && nitems == 0 && rem == 0) {
+ /*
+ * If there is some PropertyNotify with no data, then
+ * this is the signal of the selection owner that all
+ * data has been transferred. We won't need to receive
+ * PropertyNotify events anymore.
+ */
+ MODBIT(state.wattrs.event_mask, 0, PropertyChangeMask);
+ XChangeWindowAttributes(state.dpy, state.win, CWEventMask, &state.wattrs);
+ }
+
+ if (type == incratom) {
+ /*
+ * Activate the PropertyNotify events so we receive
+ * when the selection owner sends us the next
+ * chunk of data.
+ */
+ MODBIT(state.wattrs.event_mask, 1, PropertyChangeMask);
+ XChangeWindowAttributes(state.dpy, state.win, CWEventMask, &state.wattrs);
+
+ /*
+ * Deleting the property is the transfer start signal.
+ */
+ XDeleteProperty(state.dpy, state.win, (int)property);
+ continue;
+ }
+
+ /* FIXME: Is this needed for ledit? I don't think so, right? */
+ /*
+ * As seen in getsel:
+ * Line endings are inconsistent in the terminal and GUI world
+ * copy and pasting. When receiving some selection data,
+ * replace all '\n' with '\r'.
+ * FIXME: Fix the computer world.
+ */
+ /*
+ repl = data;
+ last = data + nitems * format / 8;
+ while ((repl = memchr(repl, '\n', last - repl))) {
+ *repl++ = '\r';
+ }
+ */
+
+ printf("%.*s\n", (int)(nitems * format / 8), (char*)data);
+ XFree(data);
+ /* number of 32-bit chunks returned */
+ ofs += nitems * format / 32;
+ } while (rem > 0);
+
+ /*
+ * Deleting the property again tells the selection owner to send the
+ * next data chunk in the property.
+ */
+ XDeleteProperty(state.dpy, state.win, (int)property);
+}
+
+void
+selrequest(XEvent *e)
+{
+ XSelectionRequestEvent *xsre;
+ XSelectionEvent xev;
+ Atom xa_targets, string, clipboard;
+ char *seltext;
+
+ xsre = (XSelectionRequestEvent *) e;
+ xev.type = SelectionNotify;
+ xev.requestor = xsre->requestor;
+ xev.selection = xsre->selection;
+ xev.target = xsre->target;
+ xev.time = xsre->time;
+ if (xsre->property == None)
+ xsre->property = xsre->target;
+
+ /* reject */
+ xev.property = None;
+
+ xa_targets = XInternAtom(state.dpy, "TARGETS", 0);
+ if (xsre->target == xa_targets) {
+ /* respond with the supported type */
+ string = xsel.xtarget;
+ XChangeProperty(xsre->display, xsre->requestor, xsre->property,
+ XA_ATOM, 32, PropModeReplace,
+ (unsigned char *) &string, 1);
+ xev.property = xsre->property;
+ } else if (xsre->target == xsel.xtarget || xsre->target == XA_STRING) {
+ /*
+ * xith XA_STRING non ascii characters may be incorrect in the
+ * requestor. It is not our problem, use utf8.
+ */
+ clipboard = XInternAtom(state.dpy, "CLIPBOARD", 0);
+ if (xsre->selection == XA_PRIMARY) {
+ seltext = xsel.primary;
+ } else if (xsre->selection == clipboard) {
+ seltext = xsel.clipboard;
+ } else {
+ fprintf(stderr,
+ "Unhandled clipboard selection 0x%lx\n",
+ xsre->selection);
+ return;
+ }
+ if (seltext != NULL) {
+ XChangeProperty(xsre->display, xsre->requestor,
+ xsre->property, xsre->target,
+ 8, PropModeReplace,
+ (unsigned char *)seltext, strlen(seltext));
+ xev.property = xsre->property;
+ }
+ }
+
+ /* all done, send a notification to the listener */
+ if (!XSendEvent(xsre->display, xsre->requestor, 1, 0, (XEvent *) &xev))
+ fprintf(stderr, "Error sending SelectionNotify event\n");
+}
+
static void
get_new_line_softline(
int cur_line, int cur_index, int movement,
@@ -464,6 +668,16 @@ mainloop(void) {
case ClientMessage:
if ((Atom)event.xclient.data.l[0] == state.wm_delete_msg)
running = 0;
+ break;
+ case SelectionNotify:
+ selnotify(&event);
+ break;
+ case PropertyNotify:
+ propnotify(&event);
+ break;
+ case SelectionRequest:
+ selrequest(&event);
+ break;
default:
break;
}
@@ -548,17 +762,21 @@ setup(int argc, char *argv[]) {
state.depth = DefaultDepth(state.dpy, state.screen);
state.cm = DefaultColormap(state.dpy, state.screen);
- memset(&attrs, 0, sizeof(attrs));
- attrs.background_pixel = BlackPixel(state.dpy, state.screen);
- attrs.colormap = state.cm;
+ memset(&state.wattrs, 0, sizeof(attrs));
+ state.wattrs.background_pixel = BlackPixel(state.dpy, state.screen);
+ state.wattrs.colormap = state.cm;
/* this causes the window contents to be kept
* when it is resized, leading to less flicker */
- attrs.bit_gravity = NorthWestGravity;
+ state.wattrs.bit_gravity = NorthWestGravity;
+ /* FIXME: FocusChangeMask? */
+ state.wattrs.event_mask = KeyPressMask |
+ ExposureMask | VisibilityChangeMask | StructureNotifyMask |
+ ButtonMotionMask | ButtonPressMask | ButtonReleaseMask;
state.win = XCreateWindow(
state.dpy, DefaultRootWindow(state.dpy), 0, 0,
state.w, state.h, 0, state.depth,
InputOutput, state.vis,
- CWBackPixel | CWColormap | CWBitGravity, &attrs
+ CWBackPixel | CWColormap | CWBitGravity | CWEventMask, &state.wattrs
);
XSetStandardProperties(state.dpy, state.win, "ledit", NULL, None, argv, argc, NULL);
@@ -581,17 +799,20 @@ setup(int argc, char *argv[]) {
XftColorAllocName(state.dpy, state.vis, state.cm, "#FFFFFF", &state.bg);
XftColorAllocName(state.dpy, state.vis, state.cm, "#CCCCCC", &state.scroll_bg);
+ /*
XSelectInput(
state.dpy, state.win,
StructureNotifyMask | KeyPressMask |
ButtonPressMask | ButtonReleaseMask |
PointerMotionMask | ExposureMask
);
+ */
state.wm_delete_msg = XInternAtom(state.dpy, "WM_DELETE_WINDOW", False);
XSetWMProtocols(state.dpy, state.win, &state.wm_delete_msg, 1);
/* blatantly stolen from st (simple terminal) */
+ /* FIXME: get improved input handling from newer version of st */
if ((state.xim = XOpenIM(state.dpy, NULL, NULL, NULL)) == NULL) {
XSetLocaleModifiers("@im=local");
if ((state.xim = XOpenIM(state.dpy, NULL, NULL, NULL)) == NULL) {
@@ -630,6 +851,13 @@ setup(int argc, char *argv[]) {
key_stack.len = key_stack.alloc = 0;
key_stack.stack = NULL;
+ xsel.primary = NULL;
+ xsel.primary_alloc = 0;
+ xsel.clipboard = NULL;
+ xsel.xtarget = XInternAtom(state.dpy, "UTF8_STRING", 0);
+ if (xsel.xtarget == None)
+ xsel.xtarget = XA_STRING;
+
redraw();
}
@@ -783,6 +1011,57 @@ sort_selection(int *line1, int *byte1, int *line2, int *byte2) {
}
}
+/* FIXME: when selecting with mouse, only call this when button is released */
+/* lines and bytes need to be sorted already! */
+static void
+copy_selection_to_x_primary(int line1, int byte1, int line2, int byte2) {
+ size_t len = 0;
+ ledit_line *ll1 = ledit_get_line(buffer, line1);
+ ledit_line *ll2 = ledit_get_line(buffer, line2);
+ if (line1 == line2) {
+ len = byte2 - byte1;
+ } else {
+ /* + 1 for newline */
+ len = ll1->len - byte1 + byte2 + 1;
+ for (int i = line1 + 1; i < line2; i++) {
+ ledit_line *ll = ledit_get_line(buffer, i);
+ len += ll->len + 1;
+ }
+ }
+ len += 1; /* nul */
+ if (len > xsel.primary_alloc) {
+ /* FIXME: maybe allocate a bit more */
+ xsel.primary = ledit_realloc(xsel.primary, len);
+ xsel.primary_alloc = len;
+ }
+ if (line1 == line2) {
+ memcpy(xsel.primary, ll1->text + byte1, byte2 - byte1);
+ xsel.primary[byte2 - byte1] = '\0';
+ } else {
+ size_t cur_pos = 0;
+ memcpy(xsel.primary, ll1->text + byte1, ll1->len - byte1);
+ cur_pos += ll1->len - byte1;
+ xsel.primary[cur_pos] = '\n';
+ cur_pos++;
+ for (int i = line1 + 1; i < line2; i++) {
+ ledit_line *ll = ledit_get_line(buffer, i);
+ memcpy(xsel.primary + cur_pos, ll->text, ll->len);
+ cur_pos += ll->len;
+ xsel.primary[cur_pos] = '\n';
+ cur_pos++;
+ }
+ memcpy(xsel.primary + cur_pos, ll2->text, byte2);
+ cur_pos += byte2;
+ xsel.primary[cur_pos] = '\0';
+ }
+ XSetSelectionOwner(state.dpy, XA_PRIMARY, state.win, CurrentTime);
+ /*
+ FIXME
+ if (XGetSelectionOwner(state.dpy, XA_PRIMARY) != state.win)
+ selclear();
+ */
+}
+
static void
set_selection(int line1, int byte1, int line2, int byte2) {
if (line1 == buffer->sel.line1 && line2 == buffer->sel.line2 &&
@@ -820,6 +1099,7 @@ set_selection(int line1, int byte1, int line2, int byte2) {
}
}
}
+ copy_selection_to_x_primary(l1_new, b1_new, l2_new, b2_new);
}
buffer->sel.line1 = line1;
buffer->sel.byte1 = byte1;
@@ -1257,6 +1537,8 @@ static struct key keys_en[] = {
{"d", 0, NORMAL|VISUAL, KEY_ANY, KEY_MOTION|KEY_NUMBERALLOWED, &key_d},
{"v", 0, NORMAL, KEY_ANY, KEY_ANY, &enter_visual},
{"o", 0, VISUAL, KEY_ANY, KEY_ANY, &switch_selection_end},
+ {"y", 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &clipcopy},
+ {"p", 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &clippaste}
};
static struct key keys_ur[] = {