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 | ++++ | 
| A | Makefile |  |  | 24 | ++++++++++++++++++++++++ | 
| A | ledit.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;
+	}
+}