ledit

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

commit 78088f20cdb93ead1b980f3a4092ce0a1227aafa
Author: lumidify <nobody@lumidify.org>
Date:   Thu,  1 Apr 2021 19:31:23 +0200

Add initial work that was done without version control

Diffstat:
A.gitignore | 4++++
AMakefile | 24++++++++++++++++++++++++
Aledit.c | 859+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 887 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +tmp +ledit +*.core +*.o diff --git a/Makefile b/Makefile @@ -0,0 +1,24 @@ +.POSIX: + +NAME = ledit +VERSION = -999-prealpha0 + +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/man + +BIN = ${NAME} +SRC = ${BIN:=.c} +MAN1 = ${BIN:=.1} + +CFLAGS = -g -D_POSIX_C_SOURCE=200809L `pkg-config --cflags x11 xkbfile pangoxft xext` +LDFLAGS += `pkg-config --libs x11 xkbfile pangoxft xext` -lm + +all: ${BIN} + +.c: + ${CC} ${CFLAGS} ${LDFLAGS} -o $@ $< + +clean: + rm -f ${BIN} + +.PHONY: all clean diff --git a/ledit.c b/ledit.c @@ -0,0 +1,859 @@ +#include <math.h> +#include <stdio.h> +#include <errno.h> +#include <string.h> +#include <stdlib.h> +#include <limits.h> +#include <unistd.h> +#include <locale.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/keysym.h> +#include <X11/XF86keysym.h> +#include <X11/cursorfont.h> + +#include <pango/pangoxft.h> + +#include <X11/XKBlib.h> +#include <X11/extensions/XKBrules.h> +#include <X11/extensions/Xdbe.h> + +static enum mode { + NORMAL = 1, + INSERT = 2, + VISUAL = 4 +} cur_mode = INSERT; + +struct key { + char *text; /* for keys that correspond with text */ + KeySym keysym; /* for other keys, e.g. arrow keys */ + enum mode modes; /* modes in which this keybinding is functional */ + void (*func)(void); /* callback function */ +}; + +static struct { + Display *dpy; + GC gc; + Window win; + XdbeBackBuffer back_buf; + Visual *vis; + PangoFontMap *fontmap; + PangoContext *context; + PangoFontDescription *font; + Colormap cm; + int screen; + int depth; + XIM xim; + XIC xic; + int w; + int h; + XftColor fg; + XftColor bg; + + Atom wm_delete_msg; +} state; + +static void mainloop(void); +static void setup(int argc, char *argv[]); +static void cleanup(void); +static void redraw(void); +static void drag_motion(XEvent event); +static void resize_window(int w, int h); +static void button_release(void); +static void button_press(XEvent event); +static void key_press(XEvent event); + +int +main(int argc, char *argv[]) { + setup(argc, argv); + mainloop(); + cleanup(); + + return 0; +} + +static struct line { + PangoLayout *layout; + XftDraw *draw; + char *text; + size_t cap; + size_t len; + Pixmap pix; + int w; + int h; + int pix_w; + int pix_h; + char dirty; +} *lines = NULL; +static size_t lines_num = 0; +static size_t lines_cap = 0; + +static int cur_line = 0; +static int cur_subline = 0; +static int cur_index = 0; +static int trailing = 0; +static int total_height = 0; +static int cur_display_offset = 0; + +static void +init_line(struct line *l) { + /* FIXME: check that layout created properly */ + l->layout = pango_layout_new(state.context); + pango_layout_set_width(l->layout, (state.w - 10) * PANGO_SCALE); + pango_layout_set_font_description(l->layout, state.font); + pango_layout_set_wrap(l->layout, PANGO_WRAP_WORD_CHAR); + l->text = NULL; + l->cap = l->len = 0; + l->pix = None; + /* FIXME: does this set line height reasonably when no text yet? */ + pango_layout_get_pixel_size(l->layout, &l->w, &l->h); + l->dirty = 1; +} + +static void recalc_height_absolute(void); + +static void +insert_text(struct line *l, int index, char *text, int len) { + if (len == -1) + len = strlen(text); + if (l->len + len > l->cap) { + l->cap *= 2; + if (l->cap == 0) + l->cap = 2; + l->text = realloc(l->text, l->cap); + if (!l->text) exit(1); + } + memmove(l->text + index + len, l->text + index, l->len - index); + memcpy(l->text + index, text, len); + l->len += len; + pango_layout_set_text(l->layout, l->text, l->len); + recalc_height_absolute(); + l->dirty = 1; +} + +static void insert_line_entry(int index); + +static void +render_line(struct line *l) { + /* FIXME: check for <= 0 on size */ + if (l->pix == None) { + l->pix = XCreatePixmap(state.dpy, state.back_buf, l->w + 10, l->h + 10, state.depth); + l->pix_w = l->w + 10; + l->pix_h = l->h + 10; + l->draw = XftDrawCreate(state.dpy, l->pix, state.vis, state.cm); + } else if (l->pix_w < l->w || l->pix_h < l->h) { + int new_w = l->w > l->pix_w ? l->w + 10 : l->pix_w + 10; + int new_h = l->h > l->pix_h ? l->h + 10 : l->pix_h + 10; + XFreePixmap(state.dpy, l->pix); + l->pix = XCreatePixmap(state.dpy, state.back_buf, new_w, new_h, state.depth); + l->pix_w = new_w; + l->pix_h = new_h; + XftDrawChange(l->draw, l->pix); + } + XftDrawRect(l->draw, &state.bg, 0, 0, l->w, l->h); + pango_xft_render_layout(l->draw, &state.fg, l->layout, 0, 0); + l->dirty = 0; +} + +static void +append_line(int text_index, int line_index) { + if (lines_num >= lines_cap) { + lines_cap *= 2; + if (lines_cap == 0) + lines_cap = 2; + lines = realloc(lines, lines_cap * sizeof(struct line)); + if (!lines) exit(1); + } + memmove(lines + line_index + 2, lines + line_index + 1, (lines_num - (line_index + 1)) * sizeof(struct line)); + struct line *new_l = &lines[line_index + 1]; + init_line(new_l); + lines_num++; + if (text_index != -1) { + struct line *l = &lines[line_index]; + int len = l->len - text_index; + new_l->pix = None; + new_l->len = len; + new_l->cap = len + 10; + new_l->text = malloc(new_l->cap); + if (!new_l->text) exit(1); + memcpy(new_l->text, l->text + text_index, len); + l->len = text_index; + pango_layout_set_text(new_l->layout, new_l->text, new_l->len); + pango_layout_set_text(l->layout, l->text, l->len); + /* FIXME: set height here */ + } +} + +static void change_keyboard(char *lang); + +PangoAttrList *basic_attrs; + +static void +mainloop(void) { + XEvent event; + int xkb_event_type; + int major, minor; + if (!XkbQueryExtension(state.dpy, 0, &xkb_event_type, NULL, &major, &minor)) { + fprintf(stderr, "XKB not supported."); + exit(1); + } + printf("XKB (%d.%d) supported.\n", major, minor); + /* This should select the events when the keyboard mapping changes. + When e.g. 'setxkbmap us' is executed, two events are sent, but I haven't figured out how to + change that. When the xkb layout switching is used (e.g. 'setxkbmap -option grp:shifts_toggle'), + this issue does not occur because only a state event is sent. */ + XkbSelectEvents(state.dpy, XkbUseCoreKbd, XkbNewKeyboardNotifyMask, XkbNewKeyboardNotifyMask); + XkbSelectEventDetails(state.dpy, XkbUseCoreKbd, XkbStateNotify, XkbAllStateComponentsMask, XkbGroupStateMask); + XSync(state.dpy, False); + int running = 1; + int change_kbd = 0; + + + /*draw = XftDrawCreate(state.dpy, state.back_buf, state.vis, state.cm);*/ + state.fontmap = pango_xft_get_font_map(state.dpy, state.screen); + state.context = pango_font_map_create_context(state.fontmap); + + state.font = pango_font_description_from_string("Monospace"); + pango_font_description_set_size(state.font, 16 * PANGO_SCALE); + + basic_attrs = pango_attr_list_new(); + PangoAttribute *no_hyphens = pango_attr_insert_hyphens_new(FALSE); + pango_attr_list_insert(basic_attrs, no_hyphens); + + append_line(-1, -1); + + XftColorAllocName(state.dpy, state.vis, state.cm, "#000000", &state.fg); + XftColorAllocName(state.dpy, state.vis, state.cm, "#FFFFFF", &state.bg); + int need_redraw = 0; + redraw(); + + while (running) { + do { + XNextEvent(state.dpy, &event); + if (event.type == xkb_event_type) { + change_kbd = 1; + continue; + } + if (XFilterEvent(&event, None)) + continue; + switch (event.type) { + case Expose: + redraw(); + need_redraw = 1; + break; + case ConfigureNotify: + resize_window(event.xconfigure.width, event.xconfigure.height); + if (cur_display_offset > 0 && cur_display_offset + state.h >= total_height) { + cur_display_offset = total_height - state.h; + if (cur_display_offset < 0) + cur_display_offset = 0; + } + redraw(); + need_redraw = 1; + break; + case ButtonPress: + switch (event.xbutton.button) { + case Button1: + button_press(event); + break; + case Button4: + cur_display_offset -= 10; + if (cur_display_offset < 0) + cur_display_offset = 0; + break; + case Button5: + if (cur_display_offset + state.h < total_height) { + cur_display_offset += 10; + if (cur_display_offset + state.h > total_height) + cur_display_offset = total_height - state.h; + } + break; + } + need_redraw = 1; + break; + case ButtonRelease: + if (event.xbutton.button == Button1) + button_release(); + break; + case MotionNotify: + drag_motion(event); + break; + case KeyPress: + need_redraw = 1; + key_press(event); + break; + case ClientMessage: + if ((Atom)event.xclient.data.l[0] == state.wm_delete_msg) + running = 0; + default: + break; + } + } while (XPending(state.dpy)); + + if (change_kbd) { + change_kbd = 0; + XkbStateRec s; + XkbGetState(state.dpy, XkbUseCoreKbd, &s); + XkbDescPtr desc = XkbGetKeyboard(state.dpy, XkbAllComponentsMask, XkbUseCoreKbd); + char *group = XGetAtomName(state.dpy, desc->names->groups[s.group]); + change_keyboard(group); + /*char *symbols = XGetAtomName(state.dpy, desc->names->symbols);*/ + XFree(group); + /*XFree(symbols);*/ + XkbFreeKeyboard(desc, XkbAllComponentsMask, True); + } + if (need_redraw) { + XSetForeground(state.dpy, state.gc, state.bg.pixel); + XFillRectangle(state.dpy, state.back_buf, state.gc, 0, 0, state.w, state.h); + int h = 0; + /*int cur_line_height = 0;*/ + int tmp_w, tmp_h; + int cur_line_y = 0; + int cursor_displayed = 0; + for (int i = 0; i < lines_num; i++) { + if (lines[i].dirty) { + if (i == cur_line && cur_mode == NORMAL) { + PangoAttribute *attr0 = pango_attr_background_new(0, 0, 0); + PangoAttribute *attr1 = pango_attr_foreground_new(65535, 65535, 65535); + attr0->start_index = cur_index; + attr0->end_index = cur_index + 1; + attr1->start_index = cur_index; + attr1->end_index = cur_index + 1; + PangoAttribute *attr2 = pango_attr_insert_hyphens_new(FALSE); + PangoAttrList *list = pango_attr_list_new(); + pango_attr_list_insert(list, attr0); + pango_attr_list_insert(list, attr1); + pango_attr_list_insert(list, attr2); + pango_layout_set_attributes(lines[cur_line].layout, list); + } else { + pango_layout_set_attributes(lines[i].layout, basic_attrs); + } + render_line(&lines[i]); + } + if (h + lines[i].h > cur_display_offset) { + int final_y = 0; + int dest_y = h - cur_display_offset; + int final_h = lines[i].h; + if (h < cur_display_offset) { + dest_y = 0; + final_y = cur_display_offset - h; + final_h -= cur_display_offset - h; + } + if (dest_y + final_h > state.h) { + final_h -= final_y + final_h - cur_display_offset - state.h; + } + XCopyArea(state.dpy, lines[i].pix, state.back_buf, state.gc, 0, final_y, lines[i].w, final_h, 0, dest_y); + if (i == cur_line) { + cur_line_y = h - cur_display_offset; + cursor_displayed = 1; + } + } + if (h + lines[i].h >= cur_display_offset + state.h) + break; + h += lines[i].h; + } + need_redraw = 0; + + XSetForeground(state.dpy, state.gc, BlackPixel(state.dpy, state.screen)); + PangoRectangle strong, weak; + pango_layout_get_cursor_pos(lines[cur_line].layout, cur_index, &strong, &weak); + int cursor_y = strong.y / PANGO_SCALE + cur_line_y; + if (cursor_displayed && cursor_y >= 0) { + if (cur_mode == NORMAL && cur_index == lines[cur_line].len) { + XFillRectangle( + state.dpy, state.back_buf, state.gc, + strong.x / PANGO_SCALE, cursor_y, + 10, strong.height / PANGO_SCALE + ); + } else if (cur_mode == INSERT) { + XDrawLine( + state.dpy, state.back_buf, state.gc, + strong.x / PANGO_SCALE, cursor_y, + strong.x / PANGO_SCALE, (strong.y + strong.height) / PANGO_SCALE + cur_line_y + ); + } + } + if (total_height > state.h) { + double scroll_h = ((double)state.h / total_height) * state.h; + double scroll_y = ((double)cur_display_offset / (total_height - state.h)) * (state.h - scroll_h); + XFillRectangle(state.dpy, state.back_buf, state.gc, state.w - 10, (int)round(scroll_y), 10, (int)round(scroll_h)); + } + + XdbeSwapInfo swap_info; + swap_info.swap_window = state.win; + swap_info.swap_action = XdbeBackground; + + if (!XdbeSwapBuffers(state.dpy, &swap_info, 1)) + exit(1); + XFlush(state.dpy); + } + } + pango_attr_list_unref(basic_attrs); +} + +static void +setup(int argc, char *argv[]) { + setlocale(LC_CTYPE, ""); + XSetLocaleModifiers(""); + XSetWindowAttributes attrs; + XGCValues gcv; + + state.w = 500; + state.h = 500; + state.dpy = XOpenDisplay(NULL); + state.screen = DefaultScreen(state.dpy); + + /* based on http://wili.cc/blog/xdbe.html */ + int major, minor; + if (XdbeQueryExtension(state.dpy, &major, &minor)) { + printf("Xdbe (%d.%d) supported, using double buffering.\n", major, minor); + int num_screens = 1; + Drawable screens[] = { DefaultRootWindow(state.dpy) }; + XdbeScreenVisualInfo *info = XdbeGetVisualInfo(state.dpy, screens, &num_screens); + if (!info || num_screens < 1 || info->count < 1) { + fprintf(stderr, "No visuals support Xdbe.\n"); + exit(1); + } + XVisualInfo xvisinfo_templ; + xvisinfo_templ.visualid = info->visinfo[0].visual; // We know there's at least one + xvisinfo_templ.screen = 0; + xvisinfo_templ.depth = info->visinfo[0].depth; + int matches; + XVisualInfo *xvisinfo_match = + XGetVisualInfo(state.dpy, VisualIDMask|VisualScreenMask|VisualDepthMask, &xvisinfo_templ, &matches); + if (!xvisinfo_match || matches < 1) { + fprintf(stderr, "Couldn't match a Visual with double buffering\n"); + exit(1); + } + state.vis = xvisinfo_match->visual; + } else { + fprintf(stderr, "No Xdbe support.\n"); + exit(1); + } + + state.depth = DefaultDepth(state.dpy, state.screen); + state.cm = DefaultColormap(state.dpy, state.screen); + + memset(&attrs, 0, sizeof(attrs)); + attrs.background_pixmap = None; + attrs.colormap = state.cm; + state.win = XCreateWindow(state.dpy, DefaultRootWindow(state.dpy), 0, 0, + state.w, state.h, 0, state.depth, + InputOutput, state.vis, CWBackPixmap | CWColormap, &attrs); + + state.back_buf = XdbeAllocateBackBufferName(state.dpy, state.win, XdbeBackground); + + memset(&gcv, 0, sizeof(gcv)); + gcv.line_width = 1; + state.gc = XCreateGC(state.dpy, state.back_buf, GCLineWidth, &gcv); + + XSelectInput(state.dpy, state.win, StructureNotifyMask | KeyPressMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | ExposureMask); + + state.wm_delete_msg = XInternAtom(state.dpy, "WM_DELETE_WINDOW", False); + XSetWMProtocols(state.dpy, state.win, &state.wm_delete_msg, 1); + + /* blatantly stolen from st (simple terminal) */ + if ((state.xim = XOpenIM(state.dpy, NULL, NULL, NULL)) == NULL) { + XSetLocaleModifiers("@im=local"); + if ((state.xim = XOpenIM(state.dpy, NULL, NULL, NULL)) == NULL) { + XSetLocaleModifiers("@im="); + if ((state.xim = XOpenIM(state.dpy, NULL, NULL, NULL)) == NULL) { + fprintf(stderr, "XOpenIM failed. Could not open input device.\n"); + exit(1); + } + } + } + state.xic = XCreateIC(state.xim, XNInputStyle, XIMPreeditNothing + | XIMStatusNothing, XNClientWindow, state.win, + XNFocusWindow, state.win, NULL); + if (state.xic == NULL) { + fprintf(stderr, "XCreateIC failed. Could not obtain input method.\n"); + exit(1); + } + XSetICFocus(state.xic); + + XMapWindow(state.dpy, state.win); + redraw(); +} + +static void +cleanup(void) { + XDestroyWindow(state.dpy, state.win); + XCloseDisplay(state.dpy); +} + +static void +redraw(void) { + XSetForeground(state.dpy, state.gc, BlackPixel(state.dpy, state.screen)); + XFillRectangle(state.dpy, state.back_buf, state.gc, 0, 0, state.w, state.h); +} + +static void +button_press(XEvent event) { +} + +static void +button_release(void) { +} + +static void +recalc_height(void) { + /* + int w, h; + pango_layout_get_pixel_size(lines[cur_line].layout, &w, &h); + total_height += (h - cur_line_height); + */ + if (total_height < 0) + total_height = 0; /* should never actually happen */ + /*cur_line_height = h;*/ +} + +static void +set_cur_line_height(void) { + int w, h; + pango_layout_get_pixel_size(lines[cur_line].layout, &w, &h); + lines[cur_line].h = h; + /*cur_line_height = h;*/ +} + +static void +recalc_height_absolute(void) { + int w, h; + total_height = 0; + for (int i = 0; i < lines_num; i++) { + pango_layout_get_pixel_size(lines[i].layout, &w, &h); + total_height += h; + lines[i].w = w; + lines[i].h = h; + } + set_cur_line_height(); +} + +static void +resize_window(int w, int h) { + state.w = w; + state.h = h; + total_height = 0; + int tmp_w, tmp_h; + for (int i = 0; i < lines_num; i++) { + /* 10 pixels for scrollbar */ + pango_layout_set_width(lines[i].layout, (w - 10) * PANGO_SCALE); + pango_layout_get_pixel_size(lines[i].layout, &tmp_w, &tmp_h); + total_height += tmp_h; + lines[i].h = tmp_h; + lines[i].dirty = 1; + } + //set_cur_line_height(); +} + +static void +drag_motion(XEvent event) { +} + +static void +delete_line_entry(int index) { + if (index < lines_num - 1) + memmove(lines + index, lines + index + 1, (lines_num - index - 1) * sizeof(struct line)); + lines_num--; +} + +static void +backspace(void) { + if (cur_index == 0) { + if (cur_line != 0) { + struct line *l1 = &lines[cur_line - 1]; + struct line *l2 = &lines[cur_line]; + int old_len = l1->len; + insert_text(l1, l1->len, l2->text, l2->len); + delete_line_entry(cur_line); + cur_line--; + cur_index = old_len; + //total_height -= cur_line_height(); + //set_cur_line_height(); + } + } else { + int i = cur_index - 1; + struct line *l = &lines[cur_line]; + /* find valid utf8 char - this probably needs to be improved */ + while (i > 0 && ((l->text[i] & 0xC0) == 0x80)) + i--; + memmove(l->text + i, l->text + cur_index, l->len - cur_index); + l->len -= cur_index - i; + cur_index = i; + pango_layout_set_text(l->layout, l->text, l->len); + } + lines[cur_line].dirty = 1; + recalc_height_absolute(); +} + +static void +delete_key(void) { + if (cur_index == lines[cur_line].len) { + if (cur_line != lines_num - 1) { + struct line *l1 = &lines[cur_line]; + struct line *l2 = &lines[cur_line + 1]; + int old_len = l1->len; + insert_text(l1, l1->len, l2->text, l2->len); + delete_line_entry(cur_line + 1); + cur_index = old_len; + /*total_height -= cur_line_height(); + set_cur_line_height();*/ + } + } else { + int i = cur_index + 1; + struct line *l = &lines[cur_line]; + while (i < lines[cur_line].len && ((lines[cur_line].text[i] & 0xC0) == 0x80)) + i++; + memmove(l->text + cur_index, l->text + i, l->len - i); + l->len -= i - cur_index; + pango_layout_set_text(l->layout, l->text, l->len); + } + lines[cur_line].dirty = 1; + recalc_height_absolute(); +} + +static void +move_cursor(int dir) { + int last_index = cur_index; + pango_layout_move_cursor_visually(lines[cur_line].layout, TRUE, cur_index, trailing, dir, &cur_index, &trailing); + /* we don't currently support a difference between the cursor being at + the end of a soft line and the beginning of the next line */ + while (trailing > 0) { + trailing--; + cur_index++; + while (cur_index < lines[cur_line].len && ((lines[cur_line].text[cur_index] & 0xC0) == 0x80)) + cur_index++; + } + if (cur_index < 0) + cur_index = 0; + /* when in normal mode, the cursor cannot be at the very end + of the line because it's always covering a character */ + if (cur_index >= lines[cur_line].len) { + if (cur_mode == NORMAL) + cur_index = last_index; + else + cur_index = lines[cur_line].len; + } + lines[cur_line].dirty = 1; +} + +static void +cursor_left(void) { + move_cursor(-1); +} + +static void +cursor_right(void) { + move_cursor(1); +} + +static void +return_key(void) { + append_line(cur_index, cur_line); + lines[cur_line].dirty = 1; + cur_line++; + lines[cur_line].dirty = 1; + cur_index = 0; + recalc_height_absolute(); +} + +static void +escape_key(void) { + cur_mode = NORMAL; + PangoDirection dir = PANGO_DIRECTION_RTL; + int tmp_index = cur_index; + if (cur_index >= lines[cur_line].len) + tmp_index--; + if (tmp_index >= 0) + dir = pango_layout_get_direction(lines[cur_line].layout, tmp_index); + if (dir == PANGO_DIRECTION_RTL || dir == PANGO_DIRECTION_WEAK_RTL) { + cursor_right(); + } else { + cursor_left(); + } + lines[cur_line].dirty = 1; + /* + if (cur_index > 0) + cursor_left(); + */ +} + +static void +i_key(void) { + cur_mode = INSERT; + /* + for (int i = 0; i < lines_num; i++) { + pango_layout_set_attributes(lines[i].layout, NULL); + } + */ + lines[cur_line].dirty = 1; +} + +static void +line_down(void) { + int lineno, x, trailing; + pango_layout_index_to_line_x(lines[cur_line].layout, cur_index, 0, &lineno, &x); + int maxlines = pango_layout_get_line_count(lines[cur_line].layout); + PangoLayoutLine *line = pango_layout_get_line_readonly(lines[cur_line].layout, lineno); + if (lineno == maxlines - 1) { + lines[cur_line].dirty = 1; + /* move to the next hard line */ + if (cur_line < lines_num - 1) { + cur_line++; + PangoLayoutLine *nextline = pango_layout_get_line_readonly(lines[cur_line].layout, 0); + if (pango_layout_line_x_to_index(nextline, x, &cur_index, &trailing) == FALSE) { + /* set it to *after* the last index of the line */ + cur_index = nextline->start_index + nextline->length; + } + } + } else { + /* move to the next soft line */ + PangoLayoutLine *nextline = pango_layout_get_line_readonly(lines[cur_line].layout, lineno + 1); + if (pango_layout_line_x_to_index(nextline, x, &cur_index, &trailing) == FALSE) { + /* set it to *after* the last index of the line */ + cur_index = nextline->start_index + nextline->length; + } + } + if (cur_index > 0 && cur_mode == NORMAL && cur_index >= lines[cur_line].len) + cursor_left(); + lines[cur_line].dirty = 1; +} + +static void +line_up(void) { + int lineno, x, trailing; + pango_layout_index_to_line_x(lines[cur_line].layout, cur_index, 0, &lineno, &x); + PangoLayoutLine *line = pango_layout_get_line_readonly(lines[cur_line].layout, lineno); + if (lineno == 0) { + lines[cur_line].dirty = 1; + /* move to the previous hard line */ + if (cur_line > 0) { + cur_line--; + int maxlines = pango_layout_get_line_count(lines[cur_line].layout); + PangoLayoutLine *prevline = pango_layout_get_line_readonly(lines[cur_line].layout, maxlines - 1); + if (pango_layout_line_x_to_index(prevline, x, &cur_index, &trailing) == FALSE) { + /* set it to *after* the last index of the line */ + cur_index = prevline->start_index + prevline->length; + } + } + } else { + /* move to the previous soft line */ + PangoLayoutLine *prevline = pango_layout_get_line_readonly(lines[cur_line].layout, lineno - 1); + if (pango_layout_line_x_to_index(prevline, x, &cur_index, &trailing) == FALSE) { + /* set it to *after* the last index of the line */ + cur_index = prevline->start_index + prevline->length; + } + } + if (cur_index > 0 && cur_mode == NORMAL && cur_index >= lines[cur_line].len) + cursor_left(); + lines[cur_line].dirty = 1; +} + +static void +zero_key(void) { + cur_index = 0; + lines[cur_line].dirty = 1; +} + +static struct key keys_en[] = { + {NULL, XK_BackSpace, INSERT, &backspace}, + {NULL, XK_Left, INSERT|NORMAL, &cursor_left}, + {NULL, XK_Right, INSERT|NORMAL, &cursor_right}, + {NULL, XK_Up, INSERT|NORMAL, &line_up}, + {NULL, XK_Down, INSERT|NORMAL, &line_down}, + {NULL, XK_Return, INSERT, &return_key}, + {NULL, XK_Delete, INSERT, &delete_key}, + {NULL, XK_Escape, INSERT, &escape_key}, + {"i", 0, NORMAL, &i_key}, + {"h", 0, NORMAL, &cursor_left}, + {"l", 0, NORMAL, &cursor_right}, + {"j", 0, NORMAL, &line_down}, + {"k", 0, NORMAL, &line_up}, + {"0", 0, NORMAL, &zero_key} +}; + +static struct key keys_ur[] = { + {NULL, XK_BackSpace, INSERT, &backspace}, + {NULL, XK_Left, INSERT|NORMAL, &cursor_left}, + {NULL, XK_Right, INSERT|NORMAL, &cursor_right}, + {NULL, XK_Up, INSERT|NORMAL, &line_up}, + {NULL, XK_Down, INSERT|NORMAL, &line_down}, + {NULL, XK_Return, INSERT, &return_key}, + {NULL, XK_Delete, INSERT, &delete_key}, + {NULL, XK_Escape, INSERT, &escape_key}, + {"ی", 0, NORMAL, &i_key}, + {"ح", 0, NORMAL, &cursor_left}, + {"ل", 0, NORMAL, &cursor_right}, + {"ج", 0, NORMAL, &line_down}, + {"ک", 0, NORMAL, &line_up}, + {"0", 0, NORMAL, &zero_key} +}; + +static struct key keys_hi[] = { + {NULL, XK_BackSpace, INSERT, &backspace}, + {NULL, XK_Left, INSERT|NORMAL, &cursor_left}, + {NULL, XK_Right, INSERT|NORMAL, &cursor_right}, + {NULL, XK_Up, INSERT|NORMAL, &line_up}, + {NULL, XK_Down, INSERT|NORMAL, &line_down}, + {NULL, XK_Return, INSERT, &return_key}, + {NULL, XK_Delete, INSERT, &delete_key}, + {NULL, XK_Escape, INSERT, &escape_key}, + {"ि", 0, NORMAL, &i_key}, + {"ह", 0, NORMAL, &cursor_left}, + {"ल", 0, NORMAL, &cursor_right}, + {"ज", 0, NORMAL, &line_down}, + {"क", 0, NORMAL, &line_up}, + {"0", 0, NORMAL, &zero_key} +}; + +#define LENGTH(X) (sizeof X / sizeof X[0]) + +struct lang_keys { + char *lang; + struct key *keys; + int num_keys; +}; + +static struct lang_keys keys[] = { + {"English (US)", keys_en, LENGTH(keys_en)}, + {"German", keys_en, LENGTH(keys_en)}, + {"Urdu (Pakistan)", keys_ur, LENGTH(keys_ur)}, + {"Hindi (Bolnagri)", keys_hi, LENGTH(keys_hi)} +}; + +static struct lang_keys *cur_keys = &keys[0]; + +static void change_keyboard(char *lang) { + printf("%s\n", lang); + for (int i = 0; i < LENGTH(keys); i++) { + if (!strcmp(keys[i].lang, lang)) { + cur_keys = &keys[i]; + break; + } + } +} + +static void +key_press(XEvent event) { + XWindowAttributes attrs; + char buf[32]; + KeySym sym; + /* FIXME: X_HAVE_UTF8_STRING See XmbLookupString(3) */ + int n = Xutf8LookupString(state.xic, &event.xkey, buf, sizeof(buf), &sym, NULL); + int found = 0; + for (int i = 0; i < cur_keys->num_keys; i++) { + if (cur_keys->keys[i].text) { + if (n > 0 && (cur_keys->keys[i].modes & cur_mode) && !strncmp(cur_keys->keys[i].text, buf, n)) { + cur_keys->keys[i].func(); + found = 1; + } + } else if ((cur_keys->keys[i].modes & cur_mode) && cur_keys->keys[i].keysym == sym) { + cur_keys->keys[i].func(); + found = 1; + } + } + if (cur_mode == INSERT && !found && n > 0) { + insert_text(&lines[cur_line], cur_index, buf, n); + cur_index += n; + } +}