ledit

Text editor (WIP)
git clone git://lumidify.org/ledit.git (fast, but not encrypted)
git clone https://lumidify.org/git/ledit.git (encrypted, but very slow)
Log | Files | Refs | README | LICENSE

commit 25deb9532df29d093281a2b542be2463809d7379
parent 413b7e3a74968e128ede783a25145dc5aea70c99
Author: lumidify <nobody@lumidify.org>
Date:   Mon, 17 May 2021 22:59:57 +0200

Add basic (buggy) clipboard support

Diffstat:
Mcommon.h | 1+
Mledit.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[] = {