* 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;
--- /dev/null
+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);
+
--- /dev/null
+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);
+
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;
--- /dev/null
+#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;
+}
--- /dev/null
+#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;
+ }
+}
--- /dev/null
+#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