]> git.mdlowis.com Git - projs/tide.git/commitdiff
tweaked handling of cursor keys while selection is non-null
authorMichael D. Lowis <mike@mdlowis.com>
Wed, 24 May 2017 23:56:16 +0000 (19:56 -0400)
committerMichael D. Lowis <mike@mdlowis.com>
Wed, 24 May 2017 23:56:16 +0000 (19:56 -0400)
TODO.md
inc/win.sync-conflict-20170524-185737.h [new file with mode: 0644]
inc/x11.sync-conflict-20170524-185739.h [new file with mode: 0644]
lib/view.c
lib/win.sync-conflict-20170524-185738.c [new file with mode: 0644]
lib/x11.sync-conflict-20170524-185739.c [new file with mode: 0644]
xedit.sync-conflict-20170524-185737.c [new file with mode: 0644]

diff --git a/TODO.md b/TODO.md
index db3604a1c6407078ff9f515115be3f082192a744..6daf18e8fe3066152c49f453462662d704d9b56d 100644 (file)
--- a/TODO.md
+++ b/TODO.md
@@ -4,7 +4,6 @@ Up Next:
 
 * use transaction ids to only mark buffer dirty when it really is
 * Make Fn keys execute nth command in the tags buffers
-* arrow keys with selection should clear the selection instead of moving.
 * move by words is inconsistent. Example:
     var infoId = 'readerinfo'+reader.id;
 
diff --git a/inc/win.sync-conflict-20170524-185737.h b/inc/win.sync-conflict-20170524-185737.h
new file mode 100644 (file)
index 0000000..77b464a
--- /dev/null
@@ -0,0 +1,69 @@
+enum {
+    MouseLeft    = 1,
+    MouseMiddle  = 2,
+    MouseRight   = 3,
+    MouseWheelUp = 4,
+    MouseWheelDn = 5
+};
+
+typedef enum {
+    STATUS   = 0,
+    TAGS     = 1,
+    EDIT     = 2,
+    SCROLL   = 3,
+    NREGIONS = 4,
+    FOCUSED  = 4
+} WinRegion;
+
+typedef struct {
+    int mods;
+    Rune key;
+    void (*action)(void);
+} KeyBinding;
+
+typedef struct {
+    size_t x;
+    size_t y;
+    size_t height;
+    size_t width;
+    size_t csrx;
+    size_t csry;
+    bool warp_ptr;
+    View view;
+} Region;
+
+typedef void (*MouseFunc)(WinRegion id, size_t count, size_t row, size_t col);
+
+typedef struct {
+    MouseFunc left;
+    MouseFunc middle;
+    MouseFunc right;
+} MouseConfig;
+
+void win_window(char* name, void (*errfn)(char*));
+void win_dialog(char* name, void (*errfn)(char*));
+void win_loop(void);
+void win_settext(WinRegion id, char* text);
+void win_setruler(size_t ruler);
+void win_setkeys(KeyBinding* bindings);
+void win_setmouse(MouseConfig* mconfig);
+void win_warpptr(WinRegion id);
+View* win_view(WinRegion id);
+Buf* win_buf(WinRegion id);
+Sel* win_sel(WinRegion id);
+bool win_btnpressed(int btn);
+WinRegion win_getregion(void);
+bool win_setregion(WinRegion id);
+void win_setscroll(double offset, double visible);
+
+/* These functions must be implemented by any appliation that wishes
+   to use this module */
+void onshutdown(void);
+void onfocus(bool focused);
+void onupdate(void);
+void onlayout(void);
+void onscroll(double percent);
+void onmouseleft(WinRegion id, bool pressed, size_t row, size_t col);
+void onmousemiddle(WinRegion id, bool pressed, size_t row, size_t col);
+void onmouseright(WinRegion id, bool pressed, size_t row, size_t col);
+
diff --git a/inc/x11.sync-conflict-20170524-185739.h b/inc/x11.sync-conflict-20170524-185739.h
new file mode 100644 (file)
index 0000000..a758842
--- /dev/null
@@ -0,0 +1,149 @@
+typedef struct {
+    void (*redraw)(int width, int height);
+    void (*handle_key)(int mods, uint32_t rune);
+    void (*shutdown)(void);
+    void (*set_focus)(bool focus);
+    void (*mouse_drag)(int state, int x, int y);
+    void (*mouse_btn)(int state, bool pressed, int x, int y);
+    uint32_t palette[16];
+} XConfig;
+
+typedef void* XFont;
+
+typedef struct {
+    uint32_t attr; /* attributes  applied to this cell */
+    uint32_t rune; /* rune value for the cell */
+} XGlyph;
+
+typedef struct {
+    void* font;
+    uint32_t glyph;
+    short x;
+    short y;
+} XGlyphSpec;
+
+/* key definitions */
+enum Keys {
+    /* Define some runes in the private use area of unicode to represent
+     * special keys */
+    KEY_F1     = (0xE000+0),
+    KEY_F2     = (0xE000+1),
+    KEY_F3     = (0xE000+2),
+    KEY_F4     = (0xE000+3),
+    KEY_F5     = (0xE000+4),
+    KEY_F6     = (0xE000+5),
+    KEY_F7     = (0xE000+6),
+    KEY_F8     = (0xE000+7),
+    KEY_F9     = (0xE000+8),
+    KEY_F10    = (0xE000+9),
+    KEY_F11    = (0xE000+10),
+    KEY_F12    = (0xE000+11),
+    KEY_INSERT = (0xE000+12),
+    KEY_DELETE = (0xE000+13),
+    KEY_HOME   = (0xE000+14),
+    KEY_END    = (0xE000+15),
+    KEY_PGUP   = (0xE000+16),
+    KEY_PGDN   = (0xE000+17),
+    KEY_UP     = (0xE000+18),
+    KEY_DOWN   = (0xE000+19),
+    KEY_RIGHT  = (0xE000+20),
+    KEY_LEFT   = (0xE000+21),
+
+    /* ASCII Control Characters */
+    KEY_CTRL_TILDE       = 0x00,
+    KEY_CTRL_2           = 0x00,
+    KEY_CTRL_A           = 0x01,
+    KEY_CTRL_B           = 0x02,
+    KEY_CTRL_C           = 0x03,
+    KEY_CTRL_D           = 0x04,
+    KEY_CTRL_E           = 0x05,
+    KEY_CTRL_F           = 0x06,
+    KEY_CTRL_G           = 0x07,
+    KEY_BACKSPACE        = 0x08,
+    KEY_CTRL_H           = 0x08,
+    KEY_TAB              = 0x09,
+    KEY_CTRL_I           = 0x09,
+    KEY_CTRL_J           = 0x0A,
+    KEY_CTRL_K           = 0x0B,
+    KEY_CTRL_L           = 0x0C,
+    KEY_ENTER            = 0x0D,
+    KEY_CTRL_M           = 0x0D,
+    KEY_CTRL_N           = 0x0E,
+    KEY_CTRL_O           = 0x0F,
+    KEY_CTRL_P           = 0x10,
+    KEY_CTRL_Q           = 0x11,
+    KEY_CTRL_R           = 0x12,
+    KEY_CTRL_S           = 0x13,
+    KEY_CTRL_T           = 0x14,
+    KEY_CTRL_U           = 0x15,
+    KEY_CTRL_V           = 0x16,
+    KEY_CTRL_W           = 0x17,
+    KEY_CTRL_X           = 0x18,
+    KEY_CTRL_Y           = 0x19,
+    KEY_CTRL_Z           = 0x1A,
+    KEY_ESCAPE           = 0x1B,
+    KEY_CTRL_LSQ_BRACKET = 0x1B,
+    KEY_CTRL_3           = 0x1B,
+    KEY_CTRL_4           = 0x1C,
+    KEY_CTRL_BACKSLASH   = 0x1C,
+    KEY_CTRL_5           = 0x1D,
+    KEY_CTRL_RSQ_BRACKET = 0x1D,
+    KEY_CTRL_6           = 0x1E,
+    KEY_CTRL_7           = 0x1F,
+    KEY_CTRL_SLASH       = 0x1F,
+    KEY_CTRL_UNDERSCORE  = 0x1F,
+};
+
+/* Key modifier masks */
+enum {
+    ModNone       = 0,
+    ModShift      = (1 << 0),
+    ModCapsLock   = (1 << 1),
+    ModCtrl       = (1 << 2),
+    ModAlt        = (1 << 3),
+    ModNumLock    = (1 << 4),
+    ModScrollLock = (1 << 5),
+    ModWindows    = (1 << 6),
+    ModAny        = -1
+};
+
+/* Selection identifiers */
+enum {
+    PRIMARY   = 0,
+    CLIPBOARD = 1
+};
+
+void x11_init(XConfig* cfg);
+void x11_deinit(void);
+int x11_keybtnstate(void);
+bool x11_keymodsset(int mask);
+void x11_window(char* name, int width, int height);
+void x11_dialog(char* name, int height, int width);
+void x11_show(void);
+bool x11_running(void);
+void x11_flip(void);
+void x11_flush(void);
+void x11_finish(void);
+
+int x11_events_queued(void);
+bool x11_events_await(unsigned int ms);
+void x11_events_take(void);
+
+void x11_mouse_get(int* x, int* y);
+void x11_mouse_set(int x, int y);
+
+XFont x11_font_load(char* name);
+size_t x11_font_height(XFont fnt);
+size_t x11_font_width(XFont fnt);
+size_t x11_font_descent(XFont fnt);
+void x11_font_getglyph(XFont font, XGlyphSpec* spec, uint32_t rune);
+size_t x11_font_getglyphs(XGlyphSpec* specs, const XGlyph* glyphs, int len, XFont font, int x, int y);
+
+void x11_draw_rect(int color, int x, int y, int width, int height);
+void x11_draw_utf8(XFont font, int fg, int bg, int x, int y, char* str);
+void x11_draw_glyphs(int fg, int bg, XGlyphSpec* glyphs, size_t nglyphs);
+void x11_draw_utf8(XFont font, int fg, int bg, int x, int y, char* str);
+
+bool x11_sel_get(int selid, void(*cbfn)(char*));
+bool x11_sel_set(int selid, char* str);
+
index 52478033fd9187bcdd4a7ca45039fba1c82b4c7b..02aa2832d046ef08751a4d5a4a96120ad62e1933 100644 (file)
@@ -261,8 +261,15 @@ Row* view_getrow(View* view, size_t row) {
 
 void view_byrune(View* view, int move, bool extsel) {
     Sel sel = view->selection;
-    sel.end = buf_byrune(&(view->buffer), sel.end, move);
-    if (!extsel) sel.beg = sel.end;
+    if (view_selsize(view) && !extsel) {
+        if (move == RIGHT)
+            sel.beg = sel.end;
+        else
+            sel.end = sel.beg;
+    } else {
+        sel.end = buf_byrune(&(view->buffer), sel.end, move);
+        if (!extsel) sel.beg = sel.end;
+    }
     sel.col = buf_getcol(&(view->buffer), sel.end);
     view->selection = sel;
     view->sync_needed = true;
diff --git a/lib/win.sync-conflict-20170524-185738.c b/lib/win.sync-conflict-20170524-185738.c
new file mode 100644 (file)
index 0000000..f6cfc62
--- /dev/null
@@ -0,0 +1,349 @@
+#include <stdc.h>
+#include <utf.h>
+#include <edit.h>
+#include <x11.h>
+#include <win.h>
+#include <ctype.h>
+
+static void onredraw(int height, int width);
+static void oninput(int mods, Rune key);
+static void onmousedrag(int state, int x, int y);
+static void onmousebtn(int btn, bool pressed, int x, int y);
+static void onwheelup(WinRegion id, bool pressed, size_t row, size_t col);
+static void onwheeldn(WinRegion id, bool pressed, size_t row, size_t col);
+static void draw_glyphs(size_t x, size_t y, UGlyph* glyphs, size_t rlen, size_t ncols);
+static WinRegion getregion(size_t x, size_t y);
+
+static size_t Ruler = 0;
+static double ScrollOffset = 0.0;
+static double ScrollVisible = 1.0;
+static XFont Font;
+static XConfig Config = {
+    .redraw       = onredraw,
+    .handle_key   = oninput,
+    .shutdown     = onshutdown,
+    .set_focus    = onfocus,
+    .mouse_drag   = onmousedrag,
+    .mouse_btn    = onmousebtn,
+    .palette      = COLOR_PALETTE
+};
+static WinRegion Focused = EDIT;
+static Region Regions[NREGIONS] = {0};
+KeyBinding* Keys = NULL;
+
+static void win_init(void (*errfn)(char*)) {
+    for (int i = 0; i < SCROLL; i++)
+        view_init(&(Regions[i].view), NULL, errfn);
+    x11_init(&Config);
+    Font = x11_font_load(FONTNAME);
+}
+
+void win_window(char* name, void (*errfn)(char*)) {
+    win_init(errfn);
+    x11_window(name, Width, Height);
+}
+
+void win_dialog(char* name, void (*errfn)(char*)) {
+    win_init(errfn);
+    x11_dialog(name, Width, Height);
+}
+
+static bool update_focus(void) {
+    static int prev_x = 0, prev_y = 0;
+    int ptr_x, ptr_y;
+    bool changed = false;
+    /* dont change focus if any mouse buttons are pressed */
+    if ((x11_keybtnstate() & 0x1f00) == 0) {
+        x11_mouse_get(&ptr_x, &ptr_y);
+        if (prev_x != ptr_x || prev_y != ptr_y)
+            changed = win_setregion(getregion(ptr_x, ptr_y));
+        prev_x = ptr_x, prev_y = ptr_y;
+    }
+    return changed;
+}
+
+void win_loop(void) {
+    x11_show();
+    x11_flip();
+    while (x11_running()) {
+        bool pending = x11_events_await(EventTimeout);
+        int nevents  = x11_events_queued();
+        if (update_focus() || pending || nevents) {
+            x11_events_take();
+            if (x11_running())
+                x11_flip();
+        }
+        x11_flush();
+    }
+    x11_finish();
+}
+
+void win_settext(WinRegion id, char* text) {
+    View* view = win_view(id);
+    view->buffer.gapstart = view->buffer.bufstart;
+    view->buffer.gapend   = view->buffer.bufend;
+    view->selection = (Sel){0,0,0};
+    view_putstr(view, text);
+    view_selprev(view); // clear the selection
+    buf_logclear(&(view->buffer));
+}
+
+void win_setruler(size_t ruler) {
+    Ruler = ruler;
+}
+
+void win_setkeys(KeyBinding* bindings) {
+    Keys = bindings;
+}
+
+bool win_btnpressed(int btn) {
+    int btnmask = (1 << (btn + 7));
+    return ((x11_keybtnstate() & btnmask) == btnmask);
+}
+
+WinRegion win_getregion(void) {
+    return Focused;
+}
+
+bool win_setregion(WinRegion id) {
+    bool changed = true;
+    if (Focused != id && (id == TAGS || id == EDIT))
+        changed = true, Focused = id;
+    return changed;
+}
+
+void win_warpptr(WinRegion id) {
+    Regions[id].warp_ptr = true;
+}
+
+View* win_view(WinRegion id) {
+    if (id == FOCUSED) id = Focused;
+    return &(Regions[id].view);
+}
+
+Buf* win_buf(WinRegion id) {
+    if (id == FOCUSED) id = Focused;
+    return &(Regions[id].view.buffer);
+}
+
+Sel* win_sel(WinRegion id) {
+    if (id == FOCUSED) id = Focused;
+    return &(Regions[id].view.selection);
+}
+
+void win_setscroll(double offset, double visible) {
+    ScrollOffset  = offset;
+    ScrollVisible = visible;
+}
+
+static void layout(int width, int height) {
+    size_t fheight = x11_font_height(Font);
+    size_t fwidth  = x11_font_width(Font);
+    View* statview = win_view(STATUS);
+    View* tagview  = win_view(TAGS);
+    View* editview = win_view(EDIT);
+
+    /* update the text views and region positions and sizes */
+    for (int i = 0; i < SCROLL; i++) {
+        Regions[i].x      = 2;
+        Regions[i].y      = 2;
+        Regions[i].csrx   = SIZE_MAX;
+        Regions[i].csry   = SIZE_MAX;
+        Regions[i].width  = (width - 4);
+        Regions[i].height = fheight;
+    }
+
+    /* place the status region */
+    view_resize(statview, 1, Regions[STATUS].width / fwidth);
+
+    /* Place the tag region relative to status */
+    Regions[TAGS].y = 5 + Regions[STATUS].y + Regions[STATUS].height;
+    size_t maxtagrows = ((height - Regions[TAGS].y - 5) / 4) / fheight;
+    size_t tagcols    = Regions[TAGS].width / fwidth;
+    size_t tagrows    = view_limitrows(tagview, maxtagrows, tagcols);
+    Regions[TAGS].height = tagrows * fheight;
+    view_resize(tagview, tagrows, tagcols);
+
+    /* Place the scroll region relative to tags */
+    Regions[SCROLL].x      = 0;
+    Regions[SCROLL].y      = 5 + Regions[TAGS].y + Regions[TAGS].height;
+    Regions[SCROLL].height = (height - Regions[EDIT].y - 5);
+    Regions[SCROLL].width  = 5 + fwidth;
+
+    /* Place the edit region relative to tags */
+    Regions[EDIT].x      = 3 + Regions[SCROLL].width;
+    Regions[EDIT].y      = 5 + Regions[TAGS].y + Regions[TAGS].height;
+    Regions[EDIT].height = (height - Regions[EDIT].y - 5);
+    Regions[EDIT].width  = width - Regions[SCROLL].width - 5;
+    view_resize(editview, Regions[EDIT].height / fheight, Regions[EDIT].width / fwidth);
+}
+
+static void onredraw(int width, int height) {
+    size_t fheight = x11_font_height(Font);
+    size_t fwidth  = x11_font_width(Font);
+
+    layout(width, height);
+    onupdate(); // Let the user program update the status and other content
+    view_update(win_view(STATUS), &(Regions[STATUS].csrx), &(Regions[STATUS].csry));
+    view_update(win_view(TAGS), &(Regions[TAGS].csrx), &(Regions[TAGS].csry));
+    view_update(win_view(EDIT), &(Regions[EDIT].csrx), &(Regions[EDIT].csry));
+    onlayout(); // Let the user program update the scroll bar
+
+    for (int i = 0; i < SCROLL; i++) {
+        View* view = win_view(i);
+        x11_draw_rect((i == TAGS ? CLR_BASE02 : CLR_BASE03),
+            0, Regions[i].y - 3, width, Regions[i].height + 8);
+        x11_draw_rect(CLR_BASE01, 0, Regions[i].y - 3, width, 1);
+        if ((i == EDIT) && (Ruler != 0))
+            x11_draw_rect(CLR_BASE02, (Ruler+2) * fwidth, Regions[i].y-2, 1, Regions[i].height+7);
+        for (size_t y = 0; y < view->nrows; y++) {
+            Row* row = view_getrow(view, y);
+            draw_glyphs(Regions[i].x, Regions[i].y + ((y+1) * fheight), row->cols, row->rlen, row->len);
+        }
+    }
+
+    /* draw the scroll region */
+    size_t thumbreg = (Regions[SCROLL].height - Regions[SCROLL].y + 9);
+    size_t thumboff = (size_t)((thumbreg * ScrollOffset) + (Regions[SCROLL].y - 2));
+    size_t thumbsz  = (size_t)(thumbreg * ScrollVisible);
+    if (thumbsz < 5) thumbsz = 5;
+    x11_draw_rect(CLR_BASE01, Regions[SCROLL].width, Regions[SCROLL].y - 2, 1, Regions[SCROLL].height);
+    x11_draw_rect(CLR_BASE00, 0, Regions[SCROLL].y - 2, Regions[SCROLL].width, thumbreg);
+    x11_draw_rect(CLR_BASE03, 0, thumboff, Regions[SCROLL].width, thumbsz);
+
+    /* place the cursor on screen */
+    if (Regions[Focused].csrx != SIZE_MAX && Regions[Focused].csry != SIZE_MAX) {
+        x11_draw_rect(CLR_BASE3,
+            Regions[Focused].x + (Regions[Focused].csrx * fwidth),
+            Regions[Focused].y + (Regions[Focused].csry * fheight),
+            1, fheight);
+    }
+
+    /* adjust the mouse location */
+    if (Regions[Focused].warp_ptr) {
+        Regions[Focused].warp_ptr = false;
+        size_t x = Regions[Focused].x + (Regions[Focused].csrx * fwidth)  - (fwidth/2);
+        size_t y = Regions[Focused].y + (Regions[Focused].csry * fheight) + (fheight/2);
+        x11_mouse_set(x, y);
+    }
+}
+
+static void oninput(int mods, Rune key) {
+    /* mask of modifiers we don't care about */
+    mods = mods & (ModCtrl|ModAlt|ModShift);
+    /* handle the proper line endings */
+    if (key == '\r') key = '\n';
+    /* search for a key binding entry */
+    uint32_t mkey = tolower(key);
+    for (KeyBinding* bind = Keys; bind && bind->key; bind++) {
+        if ((mkey == bind->key) && (bind->mods == ModAny || bind->mods == mods)) {
+            bind->action();
+            return;
+        }
+    }
+
+    /* fallback to just inserting the rune if it doesn't fall in the private use area.
+     * the private use area is used to encode special keys */
+    if (key < 0xE000 || key > 0xF8FF) {
+        if (key == '\n' && win_view(FOCUSED)->buffer.crlf)
+            key = RUNE_CRLF;
+        view_insert(win_view(FOCUSED), true, key);
+    }
+}
+
+static void scroll_actions(int btn, bool pressed, int x, int y) {
+    size_t row = (y-Regions[SCROLL].y) / x11_font_height(Font);
+    size_t col = (x-Regions[SCROLL].x) / x11_font_width(Font);
+    switch (btn) {
+        case MouseLeft:
+            if (pressed)
+                view_scroll(win_view(EDIT), -row);
+            break;
+        case MouseMiddle:
+            if (pressed)
+                onscroll((double)(y - Regions[SCROLL].y) /
+                         (double)(Regions[SCROLL].height - Regions[SCROLL].y));
+            break;
+        case MouseRight:
+            if (pressed)
+                view_scroll(win_view(EDIT), +row);
+            break;
+        case MouseWheelUp:
+            view_scroll(win_view(EDIT), -ScrollLines);
+            break;
+        case MouseWheelDn:
+            view_scroll(win_view(EDIT), +ScrollLines);
+            break;
+    }
+}
+
+static void onmousedrag(int state, int x, int y) {
+    WinRegion id = getregion(x, y);
+    size_t row = (y-Regions[id].y) / x11_font_height(Font);
+    size_t col = (x-Regions[id].x) / x11_font_width(Font);
+    if (id == Focused && win_btnpressed(MouseLeft))
+        view_selext(win_view(id), row, col);
+}
+
+static void onmousebtn(int btn, bool pressed, int x, int y) {
+    WinRegion id = getregion(x, y);
+    size_t row = (y-Regions[id].y) / x11_font_height(Font);
+    size_t col = (x-Regions[id].x) / x11_font_width(Font);
+
+    if (id == SCROLL) {
+        scroll_actions(btn, pressed, x, y);
+    } else {
+        switch(btn) {
+            case MouseLeft:    onmouseleft(id, pressed, row, col);   break;
+            case MouseMiddle:  onmousemiddle(id, pressed, row, col); break;
+            case MouseRight:   onmouseright(id, pressed, row, col);  break;
+            case MouseWheelUp: onwheelup(id, pressed, row, col);     break;
+            case MouseWheelDn: onwheeldn(id, pressed, row, col);     break;
+        }
+    }
+}
+
+static void onwheelup(WinRegion id, bool pressed, size_t row, size_t col) {
+    if (!pressed) return;
+    view_scroll(win_view(id), -ScrollLines);
+}
+
+static void onwheeldn(WinRegion id, bool pressed, size_t row, size_t col) {
+    if (!pressed) return;
+    view_scroll(win_view(id), +ScrollLines);
+}
+
+static void draw_glyphs(size_t x, size_t y, UGlyph* glyphs, size_t rlen, size_t ncols) {
+    XGlyphSpec specs[rlen];
+    size_t i = 0;
+    while (rlen && i < ncols) {
+        int numspecs = 0;
+        uint32_t attr = glyphs[i].attr;
+        while (i < ncols && glyphs[i].attr == attr) {
+            x11_font_getglyph(Font, &(specs[numspecs]), glyphs[i].rune);
+            specs[numspecs].x = x;
+            specs[numspecs].y = y - x11_font_descent(Font);
+            x += x11_font_width(Font);
+            numspecs++;
+            i++;
+            /* skip over null chars which mark multi column runes */
+            for (; i < ncols && !glyphs[i].rune; i++)
+                x += x11_font_width(Font);
+        }
+        /* Draw the glyphs with the proper colors */
+        uint8_t bg = attr >> 8;
+        uint8_t fg = attr & 0xFF;
+        x11_draw_glyphs(fg, bg, specs, numspecs);
+        rlen -= numspecs;
+    }
+}
+
+static WinRegion getregion(size_t x, size_t y) {
+    for (int i = 0; i < NREGIONS; i++) {
+        size_t startx = Regions[i].x, endx = startx + Regions[i].width;
+        size_t starty = Regions[i].y, endy = starty + Regions[i].height;
+        if ((startx <= x && x <= endx) && (starty <= y && y <= endy))
+            return (WinRegion)i;
+    }
+    return NREGIONS;
+}
diff --git a/lib/x11.sync-conflict-20170524-185739.c b/lib/x11.sync-conflict-20170524-185739.c
new file mode 100644 (file)
index 0000000..38d3dc1
--- /dev/null
@@ -0,0 +1,627 @@
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+#include <X11/Xft/Xft.h>
+#include <stdc.h>
+#include <x11.h>
+#include <utf.h>
+#include <edit.h>
+#include <locale.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+static struct XSel* selfetch(Atom atom);
+static void selclear(XEvent* evnt);
+static void selnotify(XEvent* evnt);
+static void selrequest(XEvent* evnt);
+
+struct XFont {
+    struct {
+        int height;
+        int width;
+        int ascent;
+        int descent;
+        XftFont* match;
+        FcFontSet* set;
+        FcPattern* pattern;
+    } base;
+    struct {
+        XftFont* font;
+        uint32_t unicodep;
+    } cache[FontCacheSize];
+    int ncached;
+};
+
+static bool Running = true;
+static struct {
+    Window root;
+    Display* display;
+    Visual* visual;
+    Colormap colormap;
+    unsigned depth;
+    int screen;
+    /* assume a single window for now. these are it's attributes */
+    Window window;
+    XftDraw* xft;
+    Pixmap pixmap;
+    int width;
+    int height;
+    XIC xic;
+    XIM xim;
+    GC gc;
+} X;
+static XConfig* Config;
+static int KeyBtnState;
+static Atom SelTarget;
+static struct XSel {
+    char* name;
+    Atom atom;
+    char* text;
+    void (*callback)(char*);
+} Selections[] = {
+    { .name = "PRIMARY" },
+    { .name = "CLIPBOARD" },
+};
+
+static void xftcolor(XftColor* xc, uint32_t c) {
+    xc->color.alpha = 0xFF | ((c & 0xFF000000) >> 16);
+    xc->color.red   = 0xFF | ((c & 0x00FF0000) >> 8);
+    xc->color.green = 0xFF | ((c & 0x0000FF00));
+    xc->color.blue  = 0xFF | ((c & 0x000000FF) << 8);
+    XftColorAllocValue(X.display, X.visual, X.colormap, &(xc->color), xc);
+}
+
+void x11_deinit(void) {
+    Running = false;
+}
+
+void x11_init(XConfig* cfg) {
+    atexit(x11_deinit);
+    signal(SIGPIPE, SIG_IGN); // Ignore the SIGPIPE signal
+    setlocale(LC_CTYPE, "");
+    XSetLocaleModifiers("");
+    /* open the X display and get basic attributes */
+    Config = cfg;
+    if (!(X.display = XOpenDisplay(0)))
+        die("could not open display");
+    X.root = DefaultRootWindow(X.display);
+    XWindowAttributes wa;
+    XGetWindowAttributes(X.display, X.root, &wa);
+    X.visual   = wa.visual;
+    X.colormap = wa.colormap;
+    X.screen   = DefaultScreen(X.display);
+    X.depth    = DefaultDepth(X.display, X.screen);
+    /* initialize selection atoms */
+    for (int i = 0; i < (sizeof(Selections) / sizeof(Selections[0])); i++)
+        Selections[i].atom = XInternAtom(X.display, Selections[i].name, 0);
+    SelTarget = XInternAtom(X.display, "UTF8_STRING", 0);
+    if (SelTarget == None)
+        SelTarget = XInternAtom(X.display, "STRING", 0);
+}
+
+int x11_keybtnstate(void) {
+    return KeyBtnState;
+}
+
+bool x11_keymodsset(int mask) {
+    return ((KeyBtnState & mask) == mask);
+}
+
+void x11_window(char* name, int width, int height) {
+    /* create the main window */
+    X.width  = width ;
+    X.height = height;
+    XWindowAttributes wa;
+    XGetWindowAttributes(X.display, X.root, &wa);
+    X.window = XCreateSimpleWindow(X.display, X.root,
+        (wa.width  - X.width) / 2,
+        (wa.height - X.height) /2,
+        X.width,
+        X.height,
+        0, X.depth,
+        Config->palette[0]);
+
+    /* register interest in the delete window message */
+    Atom wmDeleteMessage = XInternAtom(X.display, "WM_DELETE_WINDOW", False);
+    XSetWMProtocols(X.display, X.window, &wmDeleteMessage, 1);
+
+    /* setup window attributes and events */
+    XSetWindowAttributes swa;
+    swa.backing_store = WhenMapped;
+    swa.bit_gravity = NorthWestGravity;
+    XChangeWindowAttributes(X.display, X.window, CWBackingStore|CWBitGravity, &swa);
+    XStoreName(X.display, X.window, name);
+    XSelectInput(X.display, X.window,
+          StructureNotifyMask
+        | ButtonPressMask
+        | ButtonReleaseMask
+        | ButtonMotionMask
+        | KeyPressMask
+        | FocusChangeMask
+    );
+
+    /* set input methods */
+    if ((X.xim = XOpenIM(X.display, 0, 0, 0)))
+        X.xic = XCreateIC(X.xim, XNInputStyle, XIMPreeditNothing|XIMStatusNothing, XNClientWindow, X.window, XNFocusWindow, X.window, NULL);
+
+    /* initialize pixmap and drawing context */
+    X.pixmap = XCreatePixmap(X.display, X.window, width, height, X.depth);
+    X.xft    = XftDrawCreate(X.display, X.pixmap, X.visual, X.colormap);
+
+    /* initialize the graphics context */
+    XGCValues gcv;
+    gcv.foreground = WhitePixel(X.display, X.screen);
+    gcv.graphics_exposures = False;
+    X.gc = XCreateGC(X.display, X.window, GCForeground|GCGraphicsExposures, &gcv);
+}
+
+void x11_dialog(char* name, int height, int width) {
+    x11_window(name, height, width);
+    Atom WindowType = XInternAtom(X.display, "_NET_WM_WINDOW_TYPE", False);
+    Atom DialogType = XInternAtom(X.display, "_NET_WM_WINDOW_TYPE_DIALOG", False);
+    XChangeProperty(X.display, X.window, WindowType, XA_ATOM, 32, PropModeReplace, (unsigned char*)&DialogType, 1);
+}
+
+void x11_show(void) {
+    /* simulate an initial resize and map the window */
+    XConfigureEvent ce;
+    ce.type   = ConfigureNotify;
+    ce.width  = X.width;
+    ce.height = X.height;
+    XSendEvent(X.display, X.window, False, StructureNotifyMask, (XEvent *)&ce);
+    XMapWindow(X.display, X.window);
+}
+
+bool x11_running(void) {
+    return Running;
+}
+
+void x11_flip(void) {
+    Config->redraw(X.width, X.height);
+    XCopyArea(X.display, X.pixmap, X.window, X.gc, 0, 0, X.width, X.height, 0, 0);
+    x11_flush();
+}
+
+void x11_flush(void) {
+    XFlush(X.display);
+}
+
+void x11_finish(void) {
+    XCloseDisplay(X.display);
+    /* we're exiting now. If we own the clipboard, make sure it persists */
+    if (Selections[CLIPBOARD].text)
+        cmdwrite((char*[]){ "xcpd", NULL }, Selections[CLIPBOARD].text, NULL);
+}
+
+/******************************************************************************/
+
+static uint32_t special_keys(uint32_t key) {
+    switch (key) {
+        case XK_F1:        return KEY_F1;
+        case XK_F2:        return KEY_F2;
+        case XK_F3:        return KEY_F3;
+        case XK_F4:        return KEY_F4;
+        case XK_F5:        return KEY_F5;
+        case XK_F6:        return KEY_F6;
+        case XK_F7:        return KEY_F7;
+        case XK_F8:        return KEY_F8;
+        case XK_F9:        return KEY_F9;
+        case XK_F10:       return KEY_F10;
+        case XK_F11:       return KEY_F11;
+        case XK_F12:       return KEY_F12;
+        case XK_Insert:    return KEY_INSERT;
+        case XK_Delete:    return KEY_DELETE;
+        case XK_Home:      return KEY_HOME;
+        case XK_End:       return KEY_END;
+        case XK_Page_Up:   return KEY_PGUP;
+        case XK_Page_Down: return KEY_PGDN;
+        case XK_Up:        return KEY_UP;
+        case XK_Down:      return KEY_DOWN;
+        case XK_Left:      return KEY_LEFT;
+        case XK_Right:     return KEY_RIGHT;
+        case XK_Escape:    return KEY_ESCAPE;
+        case XK_BackSpace: return '\b';
+        case XK_Tab:       return '\t';
+        case XK_Return:    return '\r';
+        case XK_Linefeed:  return '\n';
+
+        /* modifiers should not trigger key presses */
+        case XK_Scroll_Lock:
+        case XK_Shift_L:
+        case XK_Shift_R:
+        case XK_Control_L:
+        case XK_Control_R:
+        case XK_Caps_Lock:
+        case XK_Shift_Lock:
+        case XK_Meta_L:
+        case XK_Meta_R:
+        case XK_Alt_L:
+        case XK_Alt_R:
+        case XK_Super_L:
+        case XK_Super_R:
+        case XK_Hyper_L:
+        case XK_Hyper_R:
+            return RUNE_ERR;
+
+        /* if it ain't special, don't touch it */
+        default:
+            return key;
+    }
+}
+
+static uint32_t getkey(XEvent* e) {
+    uint32_t rune = RUNE_ERR;
+    size_t len = 0;
+    char buf[8];
+    KeySym key;
+    Status status;
+    /* Read the key string */
+    if (X.xic)
+        len = Xutf8LookupString(X.xic, &(e->xkey), buf, sizeof(buf), &key, &status);
+    else
+        len = XLookupString(&(e->xkey), buf, sizeof(buf), &key, 0);
+    /* if it's ascii, just return it */
+    if (key >= 0x20 && key <= 0x7F)
+        return (uint32_t)key;
+    /* decode it */
+    if (len > 0) {
+        len = 0;
+        for (int i = 0; i < 8 && !utf8decode(&rune, &len, buf[i]); i++);
+    }
+    /* translate special key codes into unicode codepoints */
+    rune = special_keys(key);
+    return rune;
+}
+
+static void handle_key(XEvent* event) {
+    uint32_t key = getkey(event);
+    KeyBtnState = event->xkey.state;
+    if (key == RUNE_ERR) return;
+    Config->handle_key(KeyBtnState, key);
+}
+
+static void handle_mouse(XEvent* e) {
+    KeyBtnState = e->xbutton.state;
+    int x = e->xbutton.x;
+    int y = e->xbutton.y;
+
+    if (e->type == MotionNotify) {
+        Config->mouse_drag(KeyBtnState, x, y);
+    } else {
+        if (e->type == ButtonRelease)
+            KeyBtnState &= ~(1 << (e->xbutton.button + 7));
+        else
+            KeyBtnState |= (1 << (e->xbutton.button + 7));
+        Config->mouse_btn(e->xbutton.button, (e->type == ButtonPress), x, y);
+    }
+}
+
+static void set_focus(bool focused) {
+    if (focused) {
+        if (X.xic) XSetICFocus(X.xic);
+    } else {
+        if (X.xic) XUnsetICFocus(X.xic);
+    }
+    Config->set_focus(focused);
+}
+
+void x11_handle_event(XEvent* e) {
+    Atom wmDeleteMessage = XInternAtom(X.display, "WM_DELETE_WINDOW", False);
+    switch (e->type) {
+        case FocusIn:          set_focus(true);  break;
+        case FocusOut:         set_focus(false); break;
+        case KeyPress:         handle_key(e);    break;
+        case ButtonRelease:    handle_mouse(e);  break;
+        case ButtonPress:      handle_mouse(e);  break;
+        case MotionNotify:     handle_mouse(e);  break;
+        case SelectionClear:   selclear(e);      break;
+        case SelectionNotify:  selnotify(e);     break;
+        case SelectionRequest: selrequest(e);    break;
+        case ClientMessage:
+            if (e->xclient.data.l[0] == wmDeleteMessage)
+                Config->shutdown();
+            break;
+        case ConfigureNotify: // Resize the window
+            if (e->xconfigure.width != X.width || e->xconfigure.height != X.height) {
+                X.width  = e->xconfigure.width;
+                X.height = e->xconfigure.height;
+                X.pixmap = XCreatePixmap(X.display, X.window, X.width, X.height, X.depth);
+                X.xft    = XftDrawCreate(X.display, X.pixmap, X.visual, X.colormap);
+            }
+            break;
+    }
+}
+
+int x11_events_queued(void) {
+    return XEventsQueued(X.display, QueuedAfterFlush);
+}
+
+bool x11_events_await(unsigned int ms) {
+    fd_set fds;
+    int xfd = ConnectionNumber(X.display), redraw = 1;
+    /* configure for 100ms timeout */
+    struct timeval tv = { .tv_usec = ms * 1000 };
+    FD_ZERO(&fds);
+    FD_SET(xfd, &fds);
+    return (select(xfd+1, &fds, NULL, NULL, &tv) > 0);
+}
+
+void x11_events_take(void) {
+    XEvent e;
+    int nevents;
+    XGetMotionEvents(X.display, X.window, CurrentTime, CurrentTime, &nevents);
+    while (XPending(X.display)) {
+        XNextEvent(X.display, &e);
+        if (!XFilterEvent(&e, None))
+            x11_handle_event(&e);
+    }
+}
+
+void x11_mouse_set(int x, int y) {
+    XWarpPointer(X.display, X.window, X.window, 0, 0, X.width, X.height, x, y);
+}
+
+void x11_mouse_get(int* ptrx, int* ptry) {
+    Window xw; int x; unsigned int ux;
+    XQueryPointer(X.display, X.window, &xw, &xw, &x, &x, ptrx, ptry, &ux);
+}
+
+XFont x11_font_load(char* name) {
+    struct XFont* font = calloc(1, sizeof(struct XFont));
+    /* init the library and the base font pattern */
+    if (!FcInit())
+        die("Could not init fontconfig.\n");
+    FcPattern* pattern = FcNameParse((FcChar8 *)name);
+    if (!pattern)
+        die("can't open font %s\n", name);
+
+    /* load the base font */
+    FcResult result;
+    FcPattern* match = XftFontMatch(X.display, X.screen, pattern, &result);
+    if (!match || !(font->base.match = XftFontOpenPattern(X.display, match)))
+        die("could not load default font: %s", name);
+
+    /* get base font extents */
+    XGlyphInfo extents;
+    const FcChar8 ascii[] =
+        " !\"#$%&'()*+,-./0123456789:;<=>?"
+        "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
+        "`abcdefghijklmnopqrstuvwxyz{|}~";
+    XftTextExtentsUtf8(X.display, font->base.match, ascii, sizeof(ascii), &extents);
+    font->base.set     = NULL;
+    font->base.pattern = FcPatternDuplicate(pattern);
+    font->base.ascent  = font->base.match->ascent;
+    font->base.descent = font->base.match->descent;
+    font->base.height  = font->base.ascent + font->base.descent;
+    font->base.width   = ((extents.xOff + (sizeof(ascii) - 1)) / sizeof(ascii));
+    FcPatternDestroy(pattern);
+    return font;
+}
+
+size_t x11_font_height(XFont fnt) {
+    struct XFont* font = fnt;
+    return font->base.height;
+}
+
+size_t x11_font_width(XFont fnt) {
+    struct XFont* font = fnt;
+    return font->base.width;
+}
+
+size_t x11_font_descent(XFont fnt) {
+    struct XFont* font = fnt;
+    return font->base.descent;
+}
+
+void x11_draw_rect(int color, int x, int y, int width, int height) {
+    XftColor clr;
+    xftcolor(&clr, Config->palette[color]);
+    XftDrawRect(X.xft, &clr, x, y, width, height);
+    XftColorFree(X.display, X.visual, X.colormap, &clr);
+}
+
+void x11_font_getglyph(XFont fnt, XGlyphSpec* spec, uint32_t rune) {
+    struct XFont* font = fnt;
+    /* if the rune is in the base font, set it and return */
+    FT_UInt glyphidx = XftCharIndex(X.display, font->base.match, rune);
+    if (glyphidx) {
+        spec->font  = font->base.match;
+        spec->glyph = glyphidx;
+        return;
+    }
+    /* Otherwise check the cache */
+    for (int f = 0; f < font->ncached; f++) {
+        glyphidx = XftCharIndex(X.display, font->cache[f].font, rune);
+        /* Fond a suitable font or found a default font */
+        if (glyphidx || (!glyphidx && font->cache[f].unicodep == rune)) {
+            spec->font  = font->cache[f].font;
+            spec->glyph = glyphidx;
+            return;
+        }
+    }
+    /* if all other options fail, ask fontconfig for a suitable font */
+    FcResult fcres;
+    if (!font->base.set)
+        font->base.set = FcFontSort(0, font->base.pattern, 1, 0, &fcres);
+    FcFontSet* fcsets[]  = { font->base.set };
+    FcPattern* fcpattern = FcPatternDuplicate(font->base.pattern);
+    FcCharSet* fccharset = FcCharSetCreate();
+    FcCharSetAddChar(fccharset, rune);
+    FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset);
+    FcPatternAddBool(fcpattern, FC_SCALABLE, 1);
+    FcConfigSubstitute(0, fcpattern, FcMatchPattern);
+    FcDefaultSubstitute(fcpattern);
+    FcPattern* fontmatch = FcFontSetMatch(0, fcsets, 1, fcpattern, &fcres);
+    /* add the font to the cache and use it */
+    if (font->ncached >= FontCacheSize) {
+        font->ncached = FontCacheSize - 1;
+        XftFontClose(X.display, font->cache[font->ncached].font);
+    }
+    font->cache[font->ncached].font = XftFontOpenPattern(X.display, fontmatch);
+    font->cache[font->ncached].unicodep = rune;
+    spec->glyph = XftCharIndex(X.display, font->cache[font->ncached].font, rune);
+    spec->font  = font->cache[font->ncached].font;
+    font->ncached++;
+    FcPatternDestroy(fcpattern);
+    FcCharSetDestroy(fccharset);
+}
+
+size_t x11_font_getglyphs(XGlyphSpec* specs, const XGlyph* glyphs, int len, XFont fnt, int x, int y) {
+    struct XFont* font = fnt;
+    int winx = x, winy = y;
+    size_t numspecs = 0;
+    for (int i = 0, xp = winx, yp = winy + font->base.ascent; i < len;) {
+        x11_font_getglyph(font, &(specs[numspecs]), glyphs[i].rune);
+        specs[numspecs].x = xp;
+        specs[numspecs].y = yp;
+        xp += font->base.width;
+        numspecs++;
+        i++;
+        /* skip over null chars which mark multi column runes */
+        for (; i < len && !glyphs[i].rune; i++)
+            xp += font->base.width;
+    }
+    return numspecs;
+}
+
+void x11_draw_glyphs(int fg, int bg, XGlyphSpec* specs, size_t nspecs) {
+    if (!nspecs) return;
+    XftFont* font = specs[0].font;
+    XftColor fgc, bgc;
+    if (bg > 0) {
+        XGlyphInfo extent;
+        XftTextExtentsUtf8(X.display, font, (const FcChar8*)"0", 1, &extent);
+        int w = extent.xOff;
+        int h = (font->height - font->descent) + LineSpacing;
+        xftcolor(&bgc, Config->palette[bg]);
+        size_t width = specs[nspecs-1].x - specs[0].x + w;
+        x11_draw_rect(bg, specs[0].x, specs[0].y - h, width, font->height + LineSpacing);
+        XftColorFree(X.display, X.visual, X.colormap, &bgc);
+    }
+    xftcolor(&fgc, Config->palette[fg]);
+    XftDrawGlyphFontSpec(X.xft, &fgc, (XftGlyphFontSpec*)specs, nspecs);
+    XftColorFree(X.display, X.visual, X.colormap, &fgc);
+}
+
+void x11_draw_utf8(XFont fnt, int fg, int bg, int x, int y, char* str) {
+    struct XFont* font = fnt;
+    static XftGlyphFontSpec specs[256];
+    size_t nspecs = 0;
+    while (*str && nspecs < 256) {
+        x11_font_getglyph(font, (XGlyphSpec*)&(specs[nspecs]), *str);
+        specs[nspecs].x = x;
+        specs[nspecs].y = y;
+        x += font->base.width;
+        nspecs++;
+        str++;
+    }
+    x11_draw_glyphs(fg, bg, (XGlyphSpec*)specs, nspecs);
+}
+
+/* Selection Handling
+ *****************************************************************************/
+
+static char* readprop(Display* disp, Window win, Atom prop) {
+    Atom type;
+    int format;
+    unsigned long nitems, nleft;
+    unsigned char* ret = NULL;
+    int nread = 1024;
+
+    // Read the property in progressively larger chunks until the entire
+    // property has been read (nleft == 0)
+    do {
+        if (ret) XFree(ret);
+        XGetWindowProperty(disp, win, prop, 0, nread, False, AnyPropertyType,
+                           &type, &format, &nitems, &nleft,
+                           &ret);
+        nread *= 2;
+    } while (nleft != 0);
+
+    return (char*)ret;
+}
+
+static struct XSel* selfetch(Atom atom) {
+    for (int i = 0; i < (sizeof(Selections) / sizeof(Selections[0])); i++)
+        if (atom == Selections[i].atom)
+            return &Selections[i];
+    return NULL;
+}
+
+static void selclear(XEvent* evnt) {
+    struct XSel* sel = selfetch(evnt->xselectionrequest.selection);
+    if (!sel) return;
+    free(sel->text);
+    sel->text = NULL;
+}
+
+static void selnotify(XEvent* evnt) {
+    /* bail if the selection cannot be converted */
+    if (evnt->xselection.property == None)
+        return;
+    struct XSel* sel = selfetch( evnt->xselection.selection );
+    char* propdata = readprop(X.display, X.window, sel->atom);
+    if (evnt->xselection.target == SelTarget) {
+        void(*cbfn)(char*) = sel->callback;
+        sel->callback = NULL;
+        cbfn(propdata);
+    }
+    /* cleanup */
+    if (propdata) XFree(propdata);
+}
+
+static void selrequest(XEvent* evnt) {
+    XEvent s;
+    struct XSel* sel = selfetch( evnt->xselectionrequest.selection );
+    s.xselection.type      = SelectionNotify;
+    s.xselection.property  = evnt->xselectionrequest.property;
+    s.xselection.requestor = evnt->xselectionrequest.requestor;
+    s.xselection.selection = evnt->xselectionrequest.selection;
+    s.xselection.target    = evnt->xselectionrequest.target;
+    s.xselection.time      = evnt->xselectionrequest.time;
+
+    Atom target    = evnt->xselectionrequest.target;
+    Atom xatargets = XInternAtom(X.display, "TARGETS", 0);
+    Atom xastring  = XInternAtom(X.display, "STRING", 0);
+    if (target == xatargets) {
+        /* respond with the supported type */
+        XChangeProperty(
+            X.display,
+            s.xselection.requestor,
+            s.xselection.property,
+            XA_ATOM, 32, PropModeReplace,
+            (unsigned char*)&SelTarget, 1);
+    } else if (target == SelTarget || target == xastring) {
+        XChangeProperty(
+            X.display,
+            s.xselection.requestor,
+            s.xselection.property,
+            SelTarget, 8, PropModeReplace,
+            (unsigned char*)sel->text, strlen(sel->text));
+    }
+    XSendEvent(X.display, s.xselection.requestor, True, 0, &s);
+}
+
+bool x11_sel_get(int selid, void(*cbfn)(char*)) {
+    struct XSel* sel = &(Selections[selid]);
+    if (sel->callback) return false;
+    Window owner = XGetSelectionOwner(X.display, sel->atom);
+    if (owner == X.window) {
+        cbfn(sel->text);
+    } else if (owner != None){
+        sel->callback = cbfn;
+        XConvertSelection(X.display, sel->atom, SelTarget, sel->atom, X.window, CurrentTime);
+    }
+    return true;
+}
+
+bool x11_sel_set(int selid, char* str) {
+    struct XSel* sel = &(Selections[selid]);
+    if (!sel || !str || !*str) {
+        free(str);
+        return false;
+    } else {
+        sel->text = str;
+        XSetSelectionOwner(X.display, sel->atom, X.window, CurrentTime);
+        return true;
+    }
+}
diff --git a/xedit.sync-conflict-20170524-185737.c b/xedit.sync-conflict-20170524-185737.c
new file mode 100644 (file)
index 0000000..0897ea9
--- /dev/null
@@ -0,0 +1,526 @@
+#include <stdc.h>
+#include <x11.h>
+#include <utf.h>
+#include <edit.h>
+#include <ctype.h>
+#include <win.h>
+#include <shortcuts.h>
+
+typedef struct {
+    char* tag;
+    union {
+        void (*noarg)(void);
+        void (*arg)(char* arg);
+    } action;
+} Tag;
+
+/* The shell: Filled in with $SHELL. Used to execute commands */
+static char* ShellCmd[] = { NULL, "-c", NULL, NULL };
+static char* SedCmd[] = { "sed", "-e", NULL, NULL };
+static char* PickFileCmd[] = { "xfilepick", ".", NULL };
+static char* PickTagCmd[] = { "xtagpick", NULL, "tags", NULL, NULL };
+static char* OpenCmd[] = { "xedit", NULL, NULL };
+static Tag Builtins[];
+static int SearchDir = DOWN;
+static char* SearchTerm = NULL;
+
+/* Tag/Cmd Execution
+ ******************************************************************************/
+static Tag* tag_lookup(char* cmd) {
+    size_t len = 0;
+    Tag* tags = Builtins;
+    for (char* tag = cmd; *tag && !isspace(*tag); tag++, len++);
+    while (tags->tag) {
+        if (!strncmp(tags->tag, cmd, len))
+            return tags;
+        tags++;
+    }
+    return NULL;
+}
+
+static void tag_exec(Tag* tag, char* arg) {
+    /* if we didnt get an arg, find one in the selection */
+    if (!arg) arg = view_getstr(win_view(TAGS), NULL);
+    if (!arg) arg = view_getstr(win_view(EDIT), NULL);
+    /* execute the tag handler */
+    tag->action.arg(arg);
+    free(arg);
+}
+
+static void cmd_exec(char* cmd) {
+    char op = '\0';
+    if (*cmd == ':' || *cmd == '!' || *cmd == '<' || *cmd == '|' || *cmd == '>')
+        op = *cmd, cmd++;
+    ShellCmd[2] = cmd;
+    /* execute the command */
+    char *input = NULL, *output = NULL, *error = NULL;
+    WinRegion dest = EDIT;
+    if (op && op != '<' && op != '!' && 0 == view_selsize(win_view(EDIT)))
+        win_view(EDIT)->selection = (Sel){ .beg = 0, .end = buf_end(win_buf(EDIT)) };
+    input = view_getstr(win_view(EDIT), NULL);
+
+    if (op == '!') {
+        cmdrun(ShellCmd, NULL);
+    } else if (op == '>') {
+        dest = TAGS;
+        output = cmdwriteread(ShellCmd, input, &error);
+    } else if (op == '|') {
+        output = cmdwriteread(ShellCmd, input, &error);
+    } else if (op == ':') {
+        SedCmd[2] = cmd;
+        output = cmdwriteread(SedCmd, input, &error);
+    } else {
+        if (op != '<') dest = win_getregion();
+        output = cmdread(ShellCmd, &error);
+    }
+
+    if (error)
+        view_append(win_view(TAGS), chomp(error));
+
+    if (output) {
+        if (op == '>')
+            view_append(win_view(dest), chomp(output));
+        else
+            view_putstr(win_view(dest), output);
+        win_setregion(dest);
+    }
+    /* cleanup */
+    free(input);
+    free(output);
+    free(error);
+}
+
+static void exec(char* cmd) {
+    /* skip leading space */
+    for (; *cmd && isspace(*cmd); cmd++);
+    if (!*cmd) return;
+    /* see if it matches a builtin tag */
+    Tag* tag = tag_lookup(cmd);
+    if (tag) {
+        while (*cmd && !isspace(*cmd++));
+        tag_exec(tag, (*cmd ? stringdup(cmd) : NULL));
+    } else {
+        cmd_exec(cmd);
+    }
+}
+
+/* Action Callbacks
+ ******************************************************************************/
+static void onerror(char* msg) {
+    view_append(win_view(TAGS), msg);
+}
+
+static void trim_whitespace(void) {
+    Buf* buf = win_buf(EDIT);
+    if (TrimOnSave && buf_end(buf) > 0) {
+        View* view = win_view(EDIT);
+        unsigned off = 0, prev = 1;
+        /* loop through the buffer till we hit the end or we stop advancing */
+        while (off < buf_end(buf) && prev != off) {
+            off = buf_eol(buf, off);
+            Rune r = buf_get(buf, off-1);
+            for (; (r == ' ' || r == '\t'); r = buf_get(buf, off-1)) {
+                if (off <= view->selection.beg) {
+                    view->selection.end--;
+                    view->selection.beg--;
+                }
+                off = buf_delete(buf, off-1, off);
+            }
+            /* make sure we keep advancing */
+            prev = off;
+            off  = buf_byline(buf, off, +1);
+        }
+    }
+}
+
+static void quit(void) {
+    static uint64_t before = 0;
+    uint64_t now = getmillis();
+    if (!win_buf(EDIT)->modified || (now-before) <= 250) {
+        #ifndef TEST
+        x11_deinit();
+        #else
+        exit(0);
+        #endif
+    } else {
+        view_append(win_view(TAGS),
+            "File is modified. Repeat action twice in < 250ms to quit.");
+    }
+    before = now;
+}
+
+static bool changed_externally(Buf* buf) {
+    bool modified = (buf->modtime != modtime(buf->path));
+    if (modified) {
+        view_append(win_view(TAGS),
+            "File modified externally: Reload, Overwrite, or {SaveAs }");
+    }
+    return modified;
+}
+
+static void overwrite(void) {
+    trim_whitespace();
+    buf_save(win_buf(EDIT));
+}
+
+static void save(void) {
+    if (!changed_externally(win_buf(EDIT)))
+        overwrite();
+}
+
+static void reload(void) {
+    view_reload(win_view(EDIT));
+}
+
+/* Mouse Handling
+ ******************************************************************************/
+void onmouseleft(WinRegion id, bool pressed, size_t row, size_t col) {
+    static int count = 0;
+    static uint64_t before = 0;
+    if (!pressed) return;
+    uint64_t now = getmillis();
+    count = ((now-before) <= 250 ? count+1 : 1);
+    before = now;
+
+    if (count == 1) {
+        if (x11_keymodsset(ModShift))
+            view_selext(win_view(id), row, col);
+        else
+            view_setcursor(win_view(id), row, col);
+    } else if (count == 2) {
+        view_select(win_view(id), row, col);
+    } else if (count == 3) {
+        view_selword(win_view(id), row, col);
+    }
+}
+
+void onmousemiddle(WinRegion id, bool pressed, size_t row, size_t col) {
+    if (pressed) return;
+    if (win_btnpressed(MouseLeft)) {
+        cut();
+    } else {
+        char* str = view_fetch(win_view(id), row, col);
+        if (str) exec(str);
+        free(str);
+    }
+}
+
+void onmouseright(WinRegion id, bool pressed, size_t row, size_t col) {
+    if (pressed) return;
+    if (win_btnpressed(MouseLeft)) {
+        paste();
+    } else {
+        SearchDir *= (x11_keymodsset(ModShift) ? -1 : +1);
+        free(SearchTerm);
+        SearchTerm = view_fetch(win_view(id), row, col);
+        if (view_findstr(win_view(EDIT), SearchDir, SearchTerm)) {
+            win_setregion(EDIT);
+            win_warpptr(EDIT);
+        }
+    }
+}
+
+/* Keyboard Handling
+ ******************************************************************************/
+static void saveas(char* arg) {
+    if (arg) {
+        char* path = win_buf(EDIT)->path;
+        win_buf(EDIT)->path = stringdup(arg);
+        buf_save(win_buf(EDIT));
+        free(path);
+    }
+}
+
+static void tag_undo(void) {
+    view_undo(win_view(EDIT));
+}
+
+static void tag_redo(void) {
+    view_redo(win_view(EDIT));
+}
+
+static void search(void) {
+    char* str;
+    SearchDir *= (x11_keymodsset(ModShift) ? -1 : +1);
+    if (x11_keymodsset(ModAlt) && SearchTerm)
+        str = stringdup(SearchTerm);
+    else
+        str = view_getctx(win_view(FOCUSED));
+    view_findstr(win_view(EDIT), SearchDir, str);
+    free(SearchTerm);
+    SearchTerm = str;
+    if (view_selsize(win_view(EDIT))) {
+        win_setregion(EDIT);
+        win_warpptr(EDIT);
+    }
+}
+
+static void execute(void) {
+    char* str = view_getcmd(win_view(FOCUSED));
+    if (str) exec(str);
+    free(str);
+}
+
+static void find(char* arg) {
+    SearchDir *= (x11_keymodsset(ModShift) ? -1 : +1);
+    view_findstr(win_view(EDIT), SearchDir, arg);
+}
+
+static void open_file(void) {
+    char* file = cmdread(PickFileCmd, NULL);
+    if (file) {
+        file = chomp(file);
+        if (!win_buf(EDIT)->path && !win_buf(EDIT)->modified) {
+            buf_load(win_buf(EDIT), file);
+        } else {
+            OpenCmd[1] = file;
+            cmdrun(OpenCmd, NULL);
+        }
+    }
+    free(file);
+}
+
+static void pick_symbol(char* symbol) {
+    PickTagCmd[1] = "fetch";
+    PickTagCmd[3] = symbol;
+    char* pick = cmdread(PickTagCmd, NULL);
+    if (pick) {
+        Buf* buf = win_buf(EDIT);
+        if (buf->path && 0 == strncmp(buf->path, pick, strlen(buf->path))) {
+            view_setln(win_view(EDIT), strtoul(strrchr(pick, ':')+1, NULL, 0));
+            win_setregion(EDIT);
+        } else {
+            if (!buf->path && !buf->modified) {
+                view_init(win_view(EDIT), pick, onerror);
+            } else {
+                OpenCmd[1] = chomp(pick);
+                cmdrun(OpenCmd, NULL);
+            }
+        }
+    }
+}
+
+static void pick_ctag(void) {
+    pick_symbol(NULL);
+}
+
+static void complete(void) {
+    View* view = win_view(FOCUSED);
+    buf_getword(&(view->buffer), risword, &(view->selection));
+    view->selection.end = buf_byrune(&(view->buffer), view->selection.end, RIGHT);
+    PickTagCmd[1] = "print";
+    PickTagCmd[3] = view_getstr(view, NULL);
+    char* pick = cmdread(PickTagCmd, NULL);
+    if (pick)
+        view_putstr(view, chomp(pick));
+    free(PickTagCmd[3]);
+}
+
+static void jump_to(char* arg) {
+    if (arg) {
+        size_t line = strtoul(arg, NULL, 0);
+        if (line) {
+            view_setln(win_view(EDIT), line);
+            win_setregion(EDIT);
+        } else {
+            pick_symbol(arg);
+        }
+    }
+}
+
+static void goto_ctag(void) {
+    char* str = view_getctx(win_view(FOCUSED));
+    jump_to(str);
+    free(str);
+}
+
+static void tabs(void) {
+    bool enabled = !(win_buf(EDIT)->expand_tabs);
+    win_buf(EDIT)->expand_tabs = enabled;
+    win_buf(TAGS)->expand_tabs = enabled;
+}
+
+static void indent(void) {
+    bool enabled = !(win_buf(EDIT)->copy_indent);
+    win_buf(EDIT)->copy_indent = enabled;
+    win_buf(TAGS)->copy_indent = enabled;
+}
+
+static void del_indent(void) {
+    view_indent(win_view(FOCUSED), LEFT);
+}
+
+static void add_indent(void) {
+    view_indent(win_view(FOCUSED), RIGHT);
+}
+
+static void eol_mode(void) {
+    int crlf = win_buf(EDIT)->crlf;
+    win_buf(EDIT)->crlf = !crlf;
+    win_buf(TAGS)->crlf = !crlf;
+    exec(crlf ? "|dos2unix" : "|unix2dos");
+}
+
+static void new_win(void) {
+    cmd_exec("!edit");
+}
+
+static void newline(void) {
+    View* view = win_view(FOCUSED);
+    if (x11_keymodsset(ModShift)) {
+        view_byline(view, UP, false);
+        view_bol(view, false);
+        if (view->selection.end == 0) {
+            view_insert(view, true, '\n');
+            view->selection = (Sel){0,0,0};
+            return;
+        }
+    }
+    view_eol(view, false);
+    view_insert(view, true, '\n');
+}
+
+void highlight(void) {
+    view_selctx(win_view(FOCUSED));
+}
+
+/* Main Routine
+ ******************************************************************************/
+static Tag Builtins[] = {
+    { .tag = "Cut",       .action.noarg = cut       },
+    { .tag = "Copy",      .action.noarg = copy      },
+    { .tag = "Eol",       .action.noarg = eol_mode  },
+    { .tag = "Find",      .action.arg   = find      },
+    { .tag = "GoTo",      .action.arg   = jump_to   },
+    { .tag = "Indent",    .action.noarg = indent    },
+    { .tag = "Overwrite", .action.noarg = overwrite },
+    { .tag = "Paste",     .action.noarg = paste     },
+    { .tag = "Quit",      .action.noarg = quit      },
+    { .tag = "Redo",      .action.noarg = tag_redo  },
+    { .tag = "Reload",    .action.noarg = reload    },
+    { .tag = "Save",      .action.noarg = save      },
+    { .tag = "SaveAs",    .action.arg   = saveas    },
+    { .tag = "Tabs",      .action.noarg = tabs      },
+    { .tag = "Undo",      .action.noarg = tag_undo  },
+    { .tag = NULL,        .action.noarg = NULL      }
+};
+
+static KeyBinding Bindings[] = {
+    /* Cursor Movements */
+    { ModAny, KEY_HOME,  cursor_home  },
+    { ModAny, KEY_END,   cursor_end   },
+    { ModAny, KEY_UP,    cursor_up    },
+    { ModAny, KEY_DOWN,  cursor_dn    },
+    { ModAny, KEY_LEFT,  cursor_left  },
+    { ModAny, KEY_RIGHT, cursor_right },
+
+    /* Standard Unix Shortcuts */
+    { ModCtrl, 'u', del_to_bol  },
+    { ModCtrl, 'k', del_to_eol  },
+    { ModCtrl, 'w', del_to_bow  },
+    { ModCtrl, 'a', cursor_bol  },
+    { ModCtrl, 'e', cursor_eol  },
+
+    /* Standard Text Editing Shortcuts */
+    { ModCtrl, 's', save        },
+    { ModCtrl, 'z', undo        },
+    { ModCtrl, 'y', redo        },
+    { ModCtrl, 'x', cut         },
+    { ModCtrl, 'c', copy        },
+    { ModCtrl, 'v', paste       },
+    { ModCtrl, 'j', join_lines  },
+    { ModCtrl, 'l', select_line },
+
+    /* Block Indent */
+    { ModCtrl, '[', del_indent },
+    { ModCtrl, ']', add_indent },
+
+    /* Common Special Keys */
+    { ModNone, KEY_PGUP,      page_up   },
+    { ModNone, KEY_PGDN,      page_dn   },
+    { ModAny,  KEY_DELETE,    delete    },
+    { ModAny,  KEY_BACKSPACE, backspace },
+
+    /* Implementation Specific */
+    { ModNone,                 KEY_ESCAPE, select_prev  },
+    { ModCtrl,                 't',        change_focus },
+    { ModCtrl,                 'q',        quit         },
+    { ModCtrl,                 'h',        highlight    },
+    { ModCtrl,                 'f',        search       },
+    { ModCtrl|ModShift,        'f',        search       },
+    { ModCtrl|ModAlt,          'f',        search       },
+    { ModCtrl|ModAlt|ModShift, 'f',        search       },
+    { ModCtrl,                 'd',        execute      },
+    { ModCtrl,                 'o',        open_file    },
+    { ModCtrl,                 'p',        pick_ctag    },
+    { ModCtrl,                 'g',        goto_ctag    },
+    { ModCtrl,                 'n',        new_win      },
+    { ModCtrl,                 '\n',       newline      },
+    { ModCtrl|ModShift,        '\n',       newline      },
+    { ModCtrl,                 ' ',        complete     },
+    { 0, 0, 0 }
+};
+
+void onscroll(double percent) {
+    size_t bend = buf_end(win_buf(EDIT));
+    size_t off  = (size_t)((double)bend * percent);
+    view_scrollto(win_view(EDIT), (off >= bend ? bend : off));
+}
+
+void onfocus(bool focused) {
+    /* notify the user if the file has changed externally */
+    (void)changed_externally(win_buf(EDIT));
+}
+
+void onupdate(void) {
+    static char status_bytes[256];
+    memset(status_bytes, 0, sizeof(status_bytes));
+    char* status = status_bytes;
+    Buf* buf = win_buf(EDIT);
+    *(status++) = (buf->charset == BINARY ? 'B' : 'U');
+    *(status++) = (buf->crlf ? 'C' : 'N');
+    *(status++) = (buf->expand_tabs ? 'S' : 'T');
+    *(status++) = (buf->copy_indent ? 'I' : 'i');
+    *(status++) = (SearchDir < 0 ? '<' : '>');
+    *(status++) = (buf->modified ? '*' : ' ');
+    *(status++) = ' ';
+    char* path = (buf->path ? buf->path : "*scratch*");
+    size_t remlen = sizeof(status_bytes) - strlen(status_bytes) - 1;
+    strncat(status, path, remlen);
+    win_settext(STATUS, status_bytes);
+    win_view(STATUS)->selection = (Sel){0,0,0};
+}
+
+void onlayout(void) {
+    /* calculate and update scroll region */
+    View* view = win_view(EDIT);
+    size_t bend = buf_end(win_buf(EDIT));
+    if (bend == 0) bend = 1;
+    if (!view->rows) return;
+    size_t vbeg = view->rows[0]->off;
+    size_t vend = view->rows[view->nrows-1]->off + view->rows[view->nrows-1]->rlen;
+    double scroll_vis = (double)(vend - vbeg) / (double)bend;
+    double scroll_off = ((double)vbeg / (double)bend);
+    win_setscroll(scroll_off, scroll_vis);
+}
+
+void onshutdown(void) {
+    quit();
+}
+
+#ifndef TEST
+int main(int argc, char** argv) {
+    /* setup the shell */
+    ShellCmd[0] = getenv("SHELL");
+    if (!ShellCmd[0]) ShellCmd[0] = "/bin/sh";
+    /* Create the window and enter the event loop */
+    win_window("edit", onerror);
+    char* tags = getenv("EDITTAGS");
+    win_settext(TAGS, (tags ? tags : DEFAULT_TAGS));
+    win_setruler(80);
+    view_init(win_view(EDIT), (argc > 1 ? argv[1] : NULL), onerror);
+    win_setkeys(Bindings);
+    win_loop();
+    return 0;
+}
+#endif