URI: 
       tAdd visual mode and make current keys work with it - ledit - Text editor (WIP)
  HTML git clone git://lumidify.org/ledit.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/ledit.git (encrypted, but very slow)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 413b7e3a74968e128ede783a25145dc5aea70c99
   DIR parent 10a6b45de3f15406bb82817a1bf28c79d492145f
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Sun, 16 May 2021 20:26:05 +0200
       
       Add visual mode and make current keys work with it
       
       Diffstat:
         M buffer.c                            |      45 +++++++++++++++++--------------
         M buffer.h                            |       1 +
         M ledit.c                             |     240 ++++++++++++++++++++++---------
       
       3 files changed, 195 insertions(+), 91 deletions(-)
       ---
   DIR diff --git a/buffer.c b/buffer.c
       t@@ -360,7 +360,29 @@ ledit_x_softline_to_pos(ledit_line *line, int x, int softline, int *pos_ret) {
                }
        }
        
       -/* FIXME: cursor jumps weirdly */
       +int
       +ledit_get_legal_normal_pos(ledit_buffer *buffer, int line, int pos) {
       +        /* move back one grapheme if at end of line */
       +        int ret = pos;
       +        ledit_line *final_line = ledit_get_line(buffer, line);
       +        if (pos == final_line->len && pos > 0) {
       +                int nattrs;
       +                const PangoLogAttr *attrs =
       +                    pango_layout_get_log_attrs_readonly(final_line->layout, &nattrs);
       +                int cur = nattrs - 2;
       +                ret--;
       +                while (ret > 0 && ((final_line->text[ret] & 0xC0) == 0x80))
       +                        ret--;
       +                while (ret > 0 && cur > 0 && !attrs[cur].is_cursor_position) {
       +                        cur--;
       +                        ret--;
       +                        while (ret > 0 && ((final_line->text[ret] & 0xC0) == 0x80))
       +                                ret--;
       +                }
       +        }
       +        return ret;
       +}
       +
        /* FIXME: use at least somewhat sensible variable names */
        void
        ledit_delete_range(
       t@@ -541,24 +563,7 @@ ledit_delete_range(
                                *new_byte_ret = b1;
                                ledit_delete_line_entries(buffer, l1 + 1, l2);
                        }
       -                /* move back one grapheme if at end of line and in normal mode */
       -                ledit_line *final_line = ledit_get_line(buffer, *new_line_ret);
       -                if (buffer->state->mode == NORMAL &&
       -                    *new_byte_ret == final_line->len &&
       -                    *new_byte_ret > 0) {
       -                        int nattrs;
       -                        const PangoLogAttr *attrs =
       -                            pango_layout_get_log_attrs_readonly(final_line->layout, &nattrs);
       -                        int cur = nattrs - 2;
       -                        (*new_byte_ret)--;
       -                        while (*new_byte_ret > 0 && ((final_line->text[*new_byte_ret] & 0xC0) == 0x80))
       -                                (*new_byte_ret)--;
       -                        while (*new_byte_ret > 0 && cur > 0 && !attrs[cur].is_cursor_position) {
       -                                cur--;
       -                                (*new_byte_ret)--;
       -                                while (*new_byte_ret > 0 && ((final_line->text[*new_byte_ret] & 0xC0) == 0x80))
       -                                        (*new_byte_ret)--;
       -                        }
       -                }
       +                if (buffer->state->mode == NORMAL)
       +                        *new_byte_ret = ledit_get_legal_normal_pos(buffer, *new_line_ret, *new_byte_ret);
                }
        }
   DIR diff --git a/buffer.h b/buffer.h
       t@@ -51,6 +51,7 @@ void ledit_delete_line_entry(ledit_buffer *buffer, int index);
        ledit_line *ledit_get_line(ledit_buffer *buffer, int index);
        int ledit_line_visible(ledit_buffer *buffer, int index);
        int ledit_delete_unicode_char(ledit_buffer *buffer, int line_index, int byte_index, int dir);
       +int ledit_get_legal_normal_pos(ledit_buffer *buffer, int line, int pos);
        void ledit_delete_range(
            ledit_buffer *buffer, int line_based,
            int line_index1, int byte_index1,
   DIR diff --git a/ledit.c b/ledit.c
       t@@ -180,39 +180,63 @@ get_new_line_softline(
                }
        }
        
       +static int
       +delete_selection(void) {
       +        if (buffer->sel.line1 != buffer->sel.line2 || buffer->sel.byte1 != buffer->sel.byte2) {
       +                ledit_delete_range(
       +                    buffer, 0,
       +                    buffer->sel.line1, buffer->sel.byte1,
       +                    buffer->sel.line2, buffer->sel.byte2,
       +                    &buffer->cur_line, &buffer->cur_index
       +                );
       +                buffer->sel.line1 = buffer->sel.line2 = -1;
       +                buffer->sel.byte1 = buffer->sel.byte2 = -1;
       +                ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
       +                return 1;
       +        }
       +        return 0;
       +}
       +
        static void
        key_d(void) {
                int num = 0;
       -        struct key_stack_elem *e = pop_key_stack();
       -        if (e != NULL) {
       -                if (e->key & KEY_NUMBER) {
       -                        num = e->count;
       -                        e = pop_key_stack();
       +        if (delete_selection()) {
       +                state.mode = NORMAL;
       +                buffer->cur_index = ledit_get_legal_normal_pos(buffer, buffer->cur_line, buffer->cur_index);
       +                ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
       +                clear_key_stack();
       +        } else {
       +                struct key_stack_elem *e = pop_key_stack();
       +                if (e != NULL) {
       +                        if (e->key & KEY_NUMBER) {
       +                                num = e->count;
       +                                e = pop_key_stack();
       +                        }
       +                        /* FIXME: checking equality of the function pointer may be a bit risky */
       +                        if (e != NULL && e->motion_cb == &key_d_cb) {
       +                                int prevnum = e->count > 0 ? e->count : 1;
       +                                num = num > 0 ? num : 1;
       +                                int lines = num * prevnum;
       +                                int new_line, new_softline;
       +                                get_new_line_softline(
       +                                    buffer->cur_line, buffer->cur_index, lines - 1,
       +                                    &new_line, &new_softline
       +                                );
       +                                ledit_line *ll = ledit_get_line(buffer, new_line);
       +                                PangoLayoutLine *pl = pango_layout_get_line_readonly(ll->layout, new_softline);
       +                                e->motion_cb(new_line, pl->start_index, KEY_MOTION_LINE);
       +                                clear_key_stack();
       +                        } else if (e != NULL) {
       +                                clear_key_stack();
       +                        }
                        }
       -                /* FIXME: checking equality of the function pointer may be a bit risky */
       -                if (e != NULL && e->motion_cb == &key_d_cb) {
       -                        int prevnum = e->count > 0 ? e->count : 1;
       -                        num = num > 0 ? num : 1;
       -                        int lines = num * prevnum;
       -                        int new_line, new_softline;
       -                        get_new_line_softline(
       -                            buffer->cur_line, buffer->cur_index, lines - 1,
       -                            &new_line, &new_softline
       -                        );
       -                        ledit_line *ll = ledit_get_line(buffer, new_line);
       -                        PangoLayoutLine *pl = pango_layout_get_line_readonly(ll->layout, new_softline);
       -                        e->motion_cb(new_line, pl->start_index, KEY_MOTION_LINE);
       -                        clear_key_stack();
       -                } else if (e != NULL) {
       -                        clear_key_stack();
       +                if (e == NULL) {
       +                        e = push_key_stack();
       +                        e->key = KEY_MOTION; /* ? */
       +                        e->count = num;
       +                        e->motion_cb = &key_d_cb;
                        }
                }
       -        if (e == NULL) {
       -                e = push_key_stack();
       -                e->key = KEY_MOTION; /* ? */
       -                e->count = num;
       -                e->motion_cb = &key_d_cb;
       -        }
        }
        
        /* FIXME: should this get number of lines to remove or actual end line? */
       t@@ -239,7 +263,6 @@ key_x(void) {
                        num = e->count;
                if (num <= 0)
                        num = 1;
       -        printf("delete %d\n", num);
        }
        
        static void
       t@@ -680,7 +703,7 @@ redraw(void) {
                                    strong.x / PANGO_SCALE, cursor_y,
                                    10, strong.height / PANGO_SCALE
                                );
       -                } else if (state.mode == INSERT) {
       +                } else if (state.mode == INSERT || state.mode == VISUAL) {
                                XDrawLine(
                                    state.dpy, state.drawable, state.gc,
                                    strong.x / PANGO_SCALE, cursor_y,
       t@@ -825,6 +848,12 @@ button_press(XEvent *event) {
                                        int l, b;
                                        xy_to_line_byte(x, y, &l, &b);
                                        set_selection(l, b, l, b);
       +                                if (state.mode == NORMAL) {
       +                                        ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
       +                                        state.mode = VISUAL;
       +                                }
       +                                buffer->cur_line = l;
       +                                buffer->cur_index = b;
                                        state.selecting = 1;
                                        return 1;
                                }
       t@@ -872,6 +901,8 @@ drag_motion(XEvent *event) {
                        int y = event->xbutton.y >= 0 ? event->xbutton.y : 0;
                        xy_to_line_byte(event->xbutton.x, y, &l, &b);
                        set_selection(buffer->sel.line1, buffer->sel.byte1, l, b);
       +                buffer->cur_line = l;
       +                buffer->cur_index = b;
                        return 1;
                }
                return 0;
       t@@ -922,7 +953,9 @@ resize_window(int w, int h) {
        
        static void
        backspace(void) {
       -        if (buffer->cur_index == 0) {
       +        if (delete_selection()) {
       +                /* NOP */
       +        } else if (buffer->cur_index == 0) {
                        if (buffer->cur_line != 0) {
                                ledit_line *l1 = ledit_get_line(buffer, buffer->cur_line - 1);
                                ledit_line *l2 = ledit_get_line(buffer, buffer->cur_line);
       t@@ -946,7 +979,9 @@ backspace(void) {
        static void
        delete_key(void) {
                ledit_line *cur_line = ledit_get_line(buffer, buffer->cur_line);
       -        if (buffer->cur_index == cur_line->len) {
       +        if (delete_selection()) {
       +                /* NOP */
       +        } else if (buffer->cur_index == cur_line->len) {
                        if (buffer->cur_line != buffer->lines_num - 1) {
                                ledit_line *next_line = ledit_get_line(
                                    buffer, buffer->cur_line + 1
       t@@ -1027,7 +1062,15 @@ move_cursor_left_right(int dir) {
                        e->motion_cb(buffer->cur_line, new_index, KEY_MOTION_CHAR);
                } else {
                        buffer->cur_index = new_index;
       -                ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
       +                if (state.mode == VISUAL) {
       +                        set_selection(buffer->sel.line1, buffer->sel.byte1, buffer->sel.line2, new_index);
       +                } else if (state.mode == INSERT &&
       +                    (buffer->sel.line1 != buffer->sel.line2 ||
       +                     buffer->sel.byte1 != buffer->sel.byte2)) {
       +                        set_selection(buffer->cur_line, new_index, buffer->cur_line, new_index);
       +                } else if (state.mode == NORMAL) {
       +                        ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
       +                }
                }
                clear_key_stack();
        }
       t@@ -1044,6 +1087,7 @@ cursor_right(void) {
        
        static void
        return_key(void) {
       +        delete_selection();
                ledit_append_line(buffer, buffer->cur_line, buffer->cur_index);
                /* FIXME: these aren't needed, right? This only works in insert mode
                 * anyways, so there's nothing to wipe */
       t@@ -1055,27 +1099,36 @@ return_key(void) {
        
        static void
        escape_key(void) {
       -        state.mode = NORMAL;
       -        clear_key_stack();
       -        PangoDirection dir = PANGO_DIRECTION_RTL;
       -        int tmp_index = buffer->cur_index;
       -        ledit_line *cur_line = ledit_get_line(buffer, buffer->cur_line);
       -        if (buffer->cur_index >= cur_line->len)
       -                tmp_index--;
       -        if (tmp_index >= 0)
       -                dir = pango_layout_get_direction(cur_line->layout, tmp_index);
       -        if (dir == PANGO_DIRECTION_RTL || dir == PANGO_DIRECTION_WEAK_RTL) {
       -                cursor_right();
       +        clear_key_stack(); /* just in case... */
       +        if (state.mode == INSERT &&
       +            (buffer->sel.line1 != buffer->sel.line2 ||
       +             buffer->sel.byte1 != buffer->sel.byte2)) {
       +                state.mode = VISUAL;
                } else {
       -                cursor_left();
       +                state.mode = NORMAL;
       +                clear_key_stack();
       +                PangoDirection dir = PANGO_DIRECTION_RTL;
       +                int tmp_index = buffer->cur_index;
       +                ledit_line *cur_line = ledit_get_line(buffer, buffer->cur_line);
       +                if (buffer->cur_index >= cur_line->len)
       +                        tmp_index--;
       +                if (tmp_index >= 0)
       +                        dir = pango_layout_get_direction(cur_line->layout, tmp_index);
       +                if (dir == PANGO_DIRECTION_RTL || dir == PANGO_DIRECTION_WEAK_RTL) {
       +                        cursor_right();
       +                } else {
       +                        cursor_left();
       +                }
       +                ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
                }
       -        ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
        }
        
        static void
        enter_insert(void) {
       +        if (state.mode == NORMAL)
       +                ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
                state.mode = INSERT;
       -        ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
       +        clear_key_stack();
        }
        
        /* FIXME: Check if previous key allows motion command - or is this checked automatically before? */
       t@@ -1112,7 +1165,16 @@ move_cursor_up_down(int dir) {
                        if (buffer->cur_line != new_line)
                                ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
                        buffer->cur_line = new_line;
       -                ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
       +
       +                if (state.mode == VISUAL) {
       +                        set_selection(buffer->sel.line1, buffer->sel.byte1, buffer->cur_line, buffer->cur_index);
       +                } else if (state.mode == INSERT &&
       +                    (buffer->sel.line1 != buffer->sel.line2 ||
       +                     buffer->sel.byte1 != buffer->sel.byte2)) {
       +                        set_selection(buffer->cur_line, buffer->cur_index, buffer->cur_line, buffer->cur_index);
       +                } else if (state.mode == NORMAL) {
       +                        ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
       +                }
                }
                clear_key_stack();
        }
       t@@ -1129,37 +1191,72 @@ cursor_up(void) {
        
        static void
        cursor_to_beginning(void) {
       -        buffer->cur_index = 0;
       -        ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
       +        struct key_stack_elem *e = pop_key_stack();
       +        /* FIXME: error when no callback? */
       +        if (e != NULL & e->motion_cb != NULL) {
       +                e->motion_cb(buffer->cur_line, 0, KEY_MOTION_CHAR);
       +        } else {
       +                buffer->cur_index = 0;
       +                if (state.mode == VISUAL) {
       +                        set_selection(
       +                            buffer->sel.line1, buffer->sel.byte1,
       +                            buffer->cur_line, buffer->cur_index
       +                        );
       +                } else {
       +                        ledit_set_line_cursor_attrs(
       +                            buffer, buffer->cur_line, buffer->cur_index
       +                        );
       +                }
       +        }
       +        clear_key_stack();
       +}
       +
       +static void
       +enter_visual(void) {
       +        state.mode = VISUAL;
       +        buffer->sel.line1 = buffer->sel.line2 = buffer->cur_line;
       +        buffer->sel.byte1 = buffer->sel.byte2 = buffer->cur_index;
       +        ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
       +        clear_key_stack(); /* FIXME: error if not empty? */
       +}
       +
       +static void
       +switch_selection_end(void) {
       +        swap(&buffer->sel.line1, &buffer->sel.line2);
       +        swap(&buffer->sel.byte1, &buffer->sel.byte2);
       +        buffer->cur_line = buffer->sel.line2;
       +        buffer->cur_index = buffer->sel.byte2;
        }
        
        static struct key keys_en[] = {
                {NULL, XK_BackSpace, INSERT, KEY_ANY, KEY_ANY, &backspace},
       -        {NULL, XK_Left, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_left},
       -        {NULL, XK_Right, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_right},
       -        {NULL, XK_Up, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_up},
       -        {NULL, XK_Down, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_down},
       +        {NULL, XK_Left, VISUAL|INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_left},
       +        {NULL, XK_Right, VISUAL|INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_right},
       +        {NULL, XK_Up, VISUAL|INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_up},
       +        {NULL, XK_Down, VISUAL|INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_down},
                {NULL, XK_Return, INSERT, KEY_ANY, KEY_ANY, &return_key},
                {NULL, XK_Delete, INSERT, KEY_ANY, KEY_ANY, &delete_key},
       -        {NULL, XK_Escape, INSERT, KEY_ANY, KEY_ANY, &escape_key},
       -        {"i",  0, NORMAL, KEY_ANY, KEY_ANY, &enter_insert},
       -        {"h",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_left},
       -        {"l",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_right},
       -        {"j",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_down},
       -        {"k",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_up},
       -        {"0",  0, NORMAL, ~KEY_NUMBER, KEY_ANY, &cursor_to_beginning},
       -        {"0",  0, NORMAL, KEY_NUMBER, KEY_NUMBER, &push_0},
       -        {"1",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_1},
       -        {"2",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_2},
       -        {"3",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_3},
       -        {"4",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_4},
       -        {"5",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_5},
       -        {"6",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_6},
       -        {"7",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_7},
       -        {"8",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_8},
       -        {"9",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_9},
       +        {NULL, XK_Escape, VISUAL|INSERT, KEY_ANY, KEY_ANY, &escape_key},
       +        {"i",  0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_insert},
       +        {"h",  0, NORMAL|VISUAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_left},
       +        {"l",  0, NORMAL|VISUAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_right},
       +        {"j",  0, NORMAL|VISUAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_down},
       +        {"k",  0, NORMAL|VISUAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_up},
       +        {"0",  0, NORMAL|VISUAL, ~KEY_NUMBER, KEY_ANY, &cursor_to_beginning},
       +        {"0",  0, NORMAL|VISUAL, KEY_NUMBER, KEY_NUMBER, &push_0},
       +        {"1",  0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBER, &push_1},
       +        {"2",  0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBER, &push_2},
       +        {"3",  0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBER, &push_3},
       +        {"4",  0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBER, &push_4},
       +        {"5",  0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBER, &push_5},
       +        {"6",  0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBER, &push_6},
       +        {"7",  0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBER, &push_7},
       +        {"8",  0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBER, &push_8},
       +        {"9",  0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBER, &push_9},
                {"x",  0, NORMAL, KEY_ANY, KEY_NUMBERALLOWED, &key_x},
       -        {"d",  0, NORMAL, KEY_ANY, KEY_MOTION|KEY_NUMBERALLOWED, &key_d}
       +        {"d",  0, NORMAL|VISUAL, KEY_ANY, KEY_MOTION|KEY_NUMBERALLOWED, &key_d},
       +        {"v",  0, NORMAL, KEY_ANY, KEY_ANY, &enter_visual},
       +        {"o",  0, VISUAL, KEY_ANY, KEY_ANY, &switch_selection_end},
        };
        
        static struct key keys_ur[] = {
       t@@ -1251,6 +1348,7 @@ key_press(XEvent event) {
                                break;
                }
                if (state.mode == INSERT && !found && n > 0) {
       +                delete_selection();
                        ledit_insert_text(
                            buffer, buffer->cur_line, buffer->cur_index, buf, n
                        );