From 8324fd07493b1a034702472b49923c6a1150055a Mon Sep 17 00:00:00 2001 From: "Michael D. Lowis" Date: Wed, 24 May 2017 19:56:16 -0400 Subject: [PATCH] tweaked handling of cursor keys while selection is non-null --- TODO.md | 1 - inc/win.sync-conflict-20170524-185737.h | 69 +++ inc/x11.sync-conflict-20170524-185739.h | 149 ++++++ lib/view.c | 11 +- lib/win.sync-conflict-20170524-185738.c | 349 +++++++++++++ lib/x11.sync-conflict-20170524-185739.c | 627 ++++++++++++++++++++++++ xedit.sync-conflict-20170524-185737.c | 526 ++++++++++++++++++++ 7 files changed, 1729 insertions(+), 3 deletions(-) create mode 100644 inc/win.sync-conflict-20170524-185737.h create mode 100644 inc/x11.sync-conflict-20170524-185739.h create mode 100644 lib/win.sync-conflict-20170524-185738.c create mode 100644 lib/x11.sync-conflict-20170524-185739.c create mode 100644 xedit.sync-conflict-20170524-185737.c diff --git a/TODO.md b/TODO.md index db3604a..6daf18e 100644 --- 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 index 0000000..77b464a --- /dev/null +++ b/inc/win.sync-conflict-20170524-185737.h @@ -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 index 0000000..a758842 --- /dev/null +++ b/inc/x11.sync-conflict-20170524-185739.h @@ -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); + diff --git a/lib/view.c b/lib/view.c index 5247803..02aa283 100644 --- a/lib/view.c +++ b/lib/view.c @@ -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 index 0000000..f6cfc62 --- /dev/null +++ b/lib/win.sync-conflict-20170524-185738.c @@ -0,0 +1,349 @@ +#include +#include +#include +#include +#include +#include + +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 index 0000000..38d3dc1 --- /dev/null +++ b/lib/x11.sync-conflict-20170524-185739.c @@ -0,0 +1,627 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 index 0000000..0897ea9 --- /dev/null +++ b/xedit.sync-conflict-20170524-185737.c @@ -0,0 +1,526 @@ +#include +#include +#include +#include +#include +#include +#include + +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 -- 2.52.0