commit 99773bbc2bcaea288c5d8b657bdff74ae69e216a
parent 6b058581e1cb02dd6d4775d8ab3f008bcc87829c
Author: lumidify <nobody@lumidify.org>
Date:   Sun, 30 Jul 2023 20:50:21 +0200
Add initial incomplete implementation of text entry widget
It's currently unusable, just a basic framework is there.
Also, the basic text backend is currently broken.
Diffstat:
24 files changed, 1549 insertions(+), 254 deletions(-)
diff --git a/.ltk/ltk.cfg b/.ltk/ltk.cfg
@@ -5,7 +5,7 @@ all-activatable = true
 # text-editor = ...
 # line-editor = ...
 
-[key-binding]
+[key-binding:widget]
 # In future:
 # bind edit-text-external ...
 # bind edit-line-external ...
@@ -31,6 +31,19 @@ bind-keypress set-pressed sym return #flags run-always
 bind-keyrelease unset-pressed sym return #flags run-always
 # alternative: rawtext instead of text to ignore mapping
 
+[key-binding:entry]
+bind-keypress cursor-to-beginning sym home
+bind-keypress cursor-to-end sym end
+bind-keypress cursor-left sym left
+bind-keypress cursor-right sym right
+#bind-keypress delete-backwards sym backspace
+#bind-keypress delete-forwards sym delete
+#bind-keypress cursor-left sym left
+#bind-keypress cursor-right sym right
+#bind-keypress selection-left sym left mods shift
+#bind-keypress selection-right sym right mods shift
+#bind-keypress select-all text a mods ctrl
+
 # default mapping (just to silence warnings)
 [key-mapping]
 language = "English (US)"
diff --git a/Makefile b/Makefile
@@ -4,13 +4,14 @@
 NAME = ltk
 VERSION = -999-prealpha0
 
+# NOTE/FIXME: stb backend is currently broken
 # Note: The stb backend should not be used with untrusted font files.
 # FIXME: Using DEBUG here doesn't work because it somehow
 # interferes with a predefined macro, at least on OpenBSD.
-DEV = 0
+DEV = 1
 MEMDEBUG = 0
 SANITIZE = 0
-USE_PANGO = 0
+USE_PANGO = 1
 
 # Note: this macro magic for debugging and pango rendering seems ugly; it should probably be changed
 
@@ -50,6 +51,7 @@ OBJ = \
 	src/box.o \
 	src/scrollbar.o \
 	src/button.o \
+	src/entry.o \
 	src/label.o \
 	src/menu.o \
 	src/theme.o \
@@ -66,6 +68,7 @@ OBJ = \
 HDR = \
 	src/box.h \
 	src/button.h \
+	src/entry.h \
 	src/color.h \
 	src/grid.h \
 	src/ini.h \
@@ -90,7 +93,8 @@ HDR = \
 	src/err.h \
 	src/proto_types.h \
 	src/config.h \
-	src/widget_config.h
+	src/array.h \
+	src/keys.h
 
 all: src/ltkd src/ltkc
 
diff --git a/README.md b/README.md
@@ -6,6 +6,7 @@ WILDEST FANTASIES, NOT ACTUAL WORKING CODE.
 To build with or without pango: Follow instructions in config.mk.
 
 Note: The basic (non-pango) text doesn't work properly on all systems.
+Note: The basic (non-pango) text is currently completely broken.
 
 To test:
 
diff --git a/src/array.h b/src/array.h
@@ -0,0 +1,167 @@
+/*
+ * This file is part of the Lumidify ToolKit (LTK)
+ * Copyright (c) 2020, 2023 lumidify <nobody@lumidify.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef _LTK_ARRAY_H_
+#define _LTK_ARRAY_H_
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "util.h"
+#include "memory.h"
+
+#define LTK_ARRAY_INIT_DECL_BASE(name, type, storage)							\
+typedef struct {											\
+	type *buf;											\
+	size_t buf_size;										\
+	size_t len;											\
+} ltk_array_##name;											\
+													\
+storage ltk_array_##name *ltk_array_create_##name(size_t initial_len);					\
+storage type ltk_array_pop_##name(ltk_array_##name *ar);						\
+storage void ltk_array_prepare_gap_##name(ltk_array_##name *ar, size_t index, size_t len);		\
+storage void ltk_array_insert_##name(ltk_array_##name *ar, size_t index, type *elem, size_t len);	\
+storage void ltk_array_resize_##name(ltk_array_##name *ar, size_t size);				\
+storage void ltk_array_destroy_##name(ltk_array_##name *ar);						\
+storage void ltk_array_clear_##name(ltk_array_##name *ar);						\
+storage void ltk_array_append_##name(ltk_array_##name *ar, type elem);					\
+storage void ltk_array_destroy_deep_##name(ltk_array_##name *ar, void (*destroy_func)(type));		\
+storage type ltk_array_get_safe_##name(ltk_array_##name *ar, size_t index);				\
+storage void ltk_array_set_safe_##name(ltk_array_##name *ar, size_t index, type e);
+
+#define LTK_ARRAY_INIT_IMPL_BASE(name, type, storage)							\
+storage ltk_array_##name *										\
+ltk_array_create_##name(size_t initial_len) {								\
+	if (initial_len == 0)										\
+		ltk_fatal("Array length is zero\n");							\
+	ltk_array_##name *ar = ltk_malloc(sizeof(ltk_array_##name));					\
+	ar->buf = ltk_reallocarray(NULL, initial_len, sizeof(type));					\
+	ar->buf_size = initial_len;									\
+	ar->len = 0;											\
+	return ar;											\
+}													\
+													\
+storage type												\
+ltk_array_pop_##name(ltk_array_##name *ar) {								\
+	if (ar->len == 0) 										\
+		ltk_fatal("Array empty; cannot pop.\n");						\
+	ar->len--;											\
+	return ar->buf[ar->len];									\
+}													\
+													\
+/* FIXME: having this function in the public interface is ugly */					\
+storage void												\
+ltk_array_prepare_gap_##name(ltk_array_##name *ar, size_t index, size_t len) {				\
+	if (index > ar->len) 										\
+		ltk_fatal("Array index out of bounds\n");						\
+	ltk_array_resize_##name(ar, ar->len + len);							\
+	ar->len += len;											\
+	if (ar->len - len == index)									\
+		return;											\
+	memmove(ar->buf + index + len, ar->buf + index,							\
+	    (ar->len - len - index) * sizeof(type));							\
+}													\
+													\
+storage void												\
+ltk_array_insert_##name(ltk_array_##name *ar, size_t index, type *elem, size_t len) {			\
+	ltk_array_prepare_gap_##name(ar, index, len);							\
+	for (size_t i = 0; i < len; i++) {								\
+		ar->buf[index + i] = elem[i];								\
+	}												\
+}													\
+													\
+storage void												\
+ltk_array_append_##name(ltk_array_##name *ar, type elem) {						\
+	if (ar->len == ar->buf_size)									\
+		ltk_array_resize_##name(ar, ar->len + 1);						\
+	ar->buf[ar->len++] = elem;									\
+}													\
+													\
+storage void												\
+ltk_array_clear_##name(ltk_array_##name *ar) {								\
+	ar->len = 0;											\
+	ltk_array_resize_##name(ar, 1);									\
+}													\
+													\
+storage void												\
+ltk_array_resize_##name(ltk_array_##name *ar, size_t len) {						\
+	size_t new_size = ideal_array_size(ar->buf_size, len);						\
+	if (new_size != ar->buf_size) {									\
+		ar->buf = ltk_reallocarray(ar->buf, new_size, sizeof(type));				\
+		ar->buf_size = new_size;								\
+		ar->len = ar->len < new_size ? ar->len : new_size;					\
+	}                                                                               		\
+}													\
+													\
+storage void												\
+ltk_array_destroy_##name(ltk_array_##name *ar) {							\
+	if (!ar)											\
+		return;											\
+	ltk_free(ar->buf);										\
+	ltk_free(ar);											\
+}													\
+													\
+storage void												\
+ltk_array_destroy_deep_##name(ltk_array_##name *ar, void (*destroy_func)(type)) {			\
+	if (!ar)											\
+		return;											\
+	for (size_t i = 0; i < ar->len; i++) {								\
+		destroy_func(ar->buf[i]);								\
+	}												\
+	ltk_array_destroy_##name(ar);									\
+}													\
+													\
+storage type												\
+ltk_array_get_safe_##name(ltk_array_##name *ar, size_t index) {						\
+	if (index >= ar->len)										\
+		ltk_fatal("Index out of bounds.\n");							\
+	return ar->buf[index];										\
+}													\
+													\
+storage void												\
+ltk_array_set_safe_##name(ltk_array_##name *ar, size_t index, type e) {					\
+	if (index >= ar->len)										\
+		ltk_fatal("Index out of bounds.\n");							\
+	ar->buf[index] = e;										\
+}
+
+#define ltk_array(name) ltk_array_##name
+#define ltk_array_create(name, initial_len) ltk_array_create_##name(initial_len)
+#define ltk_array_pop(name, ar) ltk_array_pop_##name(ar)
+#define ltk_array_insert(name, ar, index, elem, len) ltk_array_insert_##name(ar, index, elem, len)
+#define ltk_array_resize(name, ar, size) ltk_array_resize_##name(ar, size)
+#define ltk_array_destroy(name, ar) ltk_array_destroy_##name(ar)
+#define ltk_array_clear(name, ar) ltk_array_clear_##name(ar)
+#define ltk_array_append(name, ar, elem) ltk_array_append_##name(ar, elem)
+#define ltk_array_destroy_deep(name, ar, destroy_func) ltk_array_destroy_deep_##name(ar, destroy_func)
+#define ltk_array_length(ar) ((ar)->len)
+#define ltk_array_get(ar, index) ((ar)->buf[index])
+#define ltk_array_get_safe(name, ar, index) ltk_array_get_safe_##name(ar, index)
+#define ltk_array_set_safe(name, ar, index, e) ltk_array_set_safe_##name(ar, index, e)
+
+#define LTK_ARRAY_INIT_DECL(name, type) LTK_ARRAY_INIT_DECL_BASE(name, type,)
+#define LTK_ARRAY_INIT_IMPL(name, type) LTK_ARRAY_INIT_IMPL_BASE(name, type,)
+#define LTK_ARRAY_INIT_DECL_STATIC(name, type) LTK_ARRAY_INIT_DECL_BASE(name, type, static)
+#define LTK_ARRAY_INIT_IMPL_STATIC(name, type) LTK_ARRAY_INIT_IMPL_BASE(name, type, static)
+
+#endif /* _LTK_ARRAY_H_ */
diff --git a/src/config.c b/src/config.c
@@ -203,6 +203,10 @@ parse_keysym(char *text, size_t len, ltk_keysym *sym_ret) {
 		*sym_ret = LTK_KEY_TAB;
 	else if (str_array_equal("escape", text, len))
 		*sym_ret = LTK_KEY_ESCAPE;
+	else if (str_array_equal("end", text, len))
+		*sym_ret = LTK_KEY_END;
+	else if (str_array_equal("home", text, len))
+		*sym_ret = LTK_KEY_HOME;
 	else
 		return 1;
 	return 0;
@@ -252,19 +256,20 @@ parse_flags(char *text, size_t len, ltk_key_binding_flags *flags_ret) {
 }
 
 static int
-parse_keypress_binding(struct lexstate *s, struct token *tok, ltk_keypress_binding *binding_ret, char **errstr) {
-	ltk_keypress_binding b = {NULL, NULL, NULL, LTK_KEY_NONE, LTK_MOD_NONE, LTK_KEY_BINDING_NOFLAGS};
+parse_keypress_binding(
+    struct lexstate *s, struct token *tok,
+    ltk_keypress_binding *binding_ret,
+    char **func_name_ret, size_t *func_len_ret,
+    char **errstr) {
+	ltk_keypress_binding b = {NULL, NULL, LTK_KEY_NONE, LTK_MOD_NONE, LTK_KEY_BINDING_NOFLAGS};
 	*tok = next_token(s);
 	char *msg = NULL;
 	if (tok->type != STRING) {
 		msg = "Invalid token type";
 		goto error;
 	}
-	b.callback = ltk_get_key_func(tok->text, tok->len);
-	if (!b.callback) {
-		msg = "Invalid function specification";
-		goto error;
-	}
+	*func_name_ret = tok->text;
+	*func_len_ret = tok->len;
 	struct token prevtok;
 	int text_init = 0, rawtext_init = 0, sym_init = 0, mods_init = 0, flags_init = 0;
 	while (1) {
@@ -364,28 +369,15 @@ error:
 	return 1;
 }
 
-static void
-push_keypress(ltk_config *c, ltk_keypress_binding b) {
-	if (c->keys.press_alloc == c->keys.press_len) {
-		c->keys.press_alloc = ideal_array_size(c->keys.press_alloc, c->keys.press_len + 1);
-		c->keys.press_bindings = ltk_reallocarray(c->keys.press_bindings, c->keys.press_alloc, sizeof(ltk_keypress_binding));
-	}
-	c->keys.press_bindings[c->keys.press_len] = b;
-	c->keys.press_len++;
-}
-
-static void
-push_keyrelease(ltk_config *c, ltk_keyrelease_binding b) {
-	if (c->keys.release_alloc == c->keys.release_len) {
-		c->keys.release_alloc = ideal_array_size(c->keys.release_alloc, c->keys.release_len + 1);
-		c->keys.release_bindings = ltk_reallocarray(c->keys.release_bindings, c->keys.release_alloc, sizeof(ltk_keyrelease_binding));
-	}
-	c->keys.release_bindings[c->keys.release_len] = b;
-	c->keys.release_len++;
-}
-
 static int
-parse_keybinding(struct lexstate *s, ltk_config *c, struct token *tok, char **errstr) {
+parse_keybinding(
+    struct lexstate *s,
+    struct token *tok,
+    char *widget,
+    size_t len,
+    keypress_binding_handler press_handler,
+    keyrelease_binding_handler release_handler,
+    char **errstr) {
 	char *msg = NULL;
 	*tok = next_token(s);
 	if (tok->type == SECTION || tok->type == END) {
@@ -397,12 +389,19 @@ parse_keybinding(struct lexstate *s, ltk_config *c, struct token *tok, char **er
 		goto error;
 	} else if (str_array_equal("bind-keypress", tok->text, tok->len)) {
 		ltk_keypress_binding b;
-		if (parse_keypress_binding(s, tok, &b, errstr))
+		char *name;
+		size_t nlen;
+		if (parse_keypress_binding(s, tok, &b, &name, &nlen, errstr))
 			return 1;
-		push_keypress(c, b);
+		if (press_handler(widget, len, name, nlen, b)) {
+			msg = "Invalid key binding";
+			goto error;
+		}
 	} else if (str_array_equal("bind-keyrelease", tok->text, tok->len)) {
 		ltk_keypress_binding b;
-		if (parse_keypress_binding(s, tok, &b, errstr))
+		char *name;
+		size_t nlen;
+		if (parse_keypress_binding(s, tok, &b, &name, &nlen, errstr))
 			return 1;
 		if (b.text || b.rawtext) {
 			free(b.text);
@@ -410,7 +409,10 @@ parse_keybinding(struct lexstate *s, ltk_config *c, struct token *tok, char **er
 			msg = "Text and rawtext may only be specified for keypress bindings";
 			goto error;
 		}
-		push_keyrelease(c, (ltk_keyrelease_binding){b.callback, b.sym, b.mods, b.flags});
+		if (release_handler(widget, len, name, nlen, (ltk_keyrelease_binding){b.sym, b.mods, b.flags})) {
+			msg = "Invalid key binding";
+			goto error;
+		}
 	} else {
 		msg = "Invalid statement";
 		goto error;
@@ -427,22 +429,22 @@ error:
 
 static void
 push_lang_mapping(ltk_config *c) {
-	if (c->keys.mappings_alloc == c->keys.mappings_len) {
-		c->keys.mappings_alloc = ideal_array_size(c->keys.mappings_alloc, c->keys.mappings_len + 1);
-		c->keys.mappings = ltk_reallocarray(c->keys.mappings, c->keys.mappings_alloc, sizeof(ltk_language_mapping));
+	if (c->mappings_alloc == c->mappings_len) {
+		c->mappings_alloc = ideal_array_size(c->mappings_alloc, c->mappings_len + 1);
+		c->mappings = ltk_reallocarray(c->mappings, c->mappings_alloc, sizeof(ltk_language_mapping));
 	}
-	c->keys.mappings[c->keys.mappings_len].lang = NULL;
-	c->keys.mappings[c->keys.mappings_len].mappings = NULL;
-	c->keys.mappings[c->keys.mappings_len].mappings_alloc = 0;
-	c->keys.mappings[c->keys.mappings_len].mappings_len = 0;
-	c->keys.mappings_len++;
+	c->mappings[c->mappings_len].lang = NULL;
+	c->mappings[c->mappings_len].mappings = NULL;
+	c->mappings[c->mappings_len].mappings_alloc = 0;
+	c->mappings[c->mappings_len].mappings_len = 0;
+	c->mappings_len++;
 }
 
 static void
 push_text_mapping(ltk_config *c, char *text1, size_t len1, char *text2, size_t len2) {
-	if (c->keys.mappings_len == 0)
+	if (c->mappings_len == 0)
 		return; /* I guess just fail silently... */
-	ltk_language_mapping *m = &c->keys.mappings[c->keys.mappings_len - 1];
+	ltk_language_mapping *m = &c->mappings[c->mappings_len - 1];
 	if (m->mappings_alloc == m->mappings_len) {
 		m->mappings_alloc = ideal_array_size(m->mappings_alloc, m->mappings_len + 1);
 		m->mappings = ltk_reallocarray(m->mappings, m->mappings_alloc, sizeof(ltk_keytext_mapping));
@@ -454,21 +456,15 @@ push_text_mapping(ltk_config *c, char *text1, size_t len1, char *text2, size_t l
 
 static void
 destroy_config(ltk_config *c) {
-	for (size_t i = 0; i < c->keys.press_len; i++) {
-		ltk_free(c->keys.press_bindings[i].text);
-		ltk_free(c->keys.press_bindings[i].rawtext);
-	}
-	ltk_free(c->keys.press_bindings);
-	ltk_free(c->keys.release_bindings);
-	for (size_t i = 0; i < c->keys.mappings_len; i++) {
-		ltk_free(c->keys.mappings[i].lang);
-		for (size_t j = 0; j < c->keys.mappings[i].mappings_len; j++) {
-			ltk_free(c->keys.mappings[i].mappings[j].from);
-			ltk_free(c->keys.mappings[i].mappings[j].to);
+	for (size_t i = 0; i < c->mappings_len; i++) {
+		ltk_free(c->mappings[i].lang);
+		for (size_t j = 0; j < c->mappings[i].mappings_len; j++) {
+			ltk_free(c->mappings[i].mappings[j].from);
+			ltk_free(c->mappings[i].mappings[j].to);
 		}
-		ltk_free(c->keys.mappings[i].mappings);
+		ltk_free(c->mappings[i].mappings);
 	}
-	ltk_free(c->keys.mappings);
+	ltk_free(c->mappings);
 	ltk_free(c);
 }
 
@@ -488,8 +484,8 @@ int
 ltk_config_get_language_index(char *lang, size_t *idx_ret) {
 	if (!global_config)
 		return 1;
-	for (size_t i = 0; i < global_config->keys.mappings_len; i++) {
-		if (!strcmp(lang, global_config->keys.mappings[i].lang)) {
+	for (size_t i = 0; i < global_config->mappings_len; i++) {
+		if (!strcmp(lang, global_config->mappings[i].lang)) {
 			*idx_ret = i;
 			return 0;
 		}
@@ -499,22 +495,32 @@ ltk_config_get_language_index(char *lang, size_t *idx_ret) {
 
 ltk_language_mapping *
 ltk_config_get_language_mapping(size_t idx) {
-	if (!global_config || idx >= global_config->keys.mappings_len)
+	if (!global_config || idx >= global_config->mappings_len)
 		return NULL;
-	return &global_config->keys.mappings[idx];
+	return &global_config->mappings[idx];
+}
+
+int
+str_array_prefix(const char *str, const char *ar, size_t len) {
+	size_t slen = strlen(str);
+	if (len < slen)
+		return 0;
+	return !strncmp(str, ar, slen);
 }
 
 /* WARNING: errstr must be freed! */
 /* FIXME: make ltk_load_file give size_t; handle errors there (copy from ledit) */
 static int
-load_from_text(const char *filename, char *file_contents, size_t len, char **errstr) {
+load_from_text(
+    const char *filename,
+    char *file_contents,
+    size_t len,
+    keypress_binding_handler press_handler,
+    keyrelease_binding_handler release_handler,
+    char **errstr) {
 	ltk_config *config = ltk_malloc(sizeof(ltk_config));
-	config->keys.press_bindings = NULL;
-	config->keys.release_bindings = NULL;
-	config->keys.mappings = NULL;
-	config->keys.press_alloc = config->keys.press_len = 0;
-	config->keys.release_alloc = config->keys.release_len = 0;
-	config->keys.mappings_alloc = config->keys.mappings_len = 0;
+	config->mappings = NULL;
+	config->mappings_alloc = config->mappings_len = 0;
 	config->general.explicit_focus = 0;
 	config->general.all_activatable = 0;
 
@@ -584,10 +590,12 @@ load_from_text(const char *filename, char *file_contents, size_t len, char **err
 					}
 					start_of_line = 1;
 				}
-			} else if (str_array_equal("key-binding", secttok.text, secttok.len)) {
+			} else if (str_array_prefix("key-binding:", secttok.text, secttok.len)) {
 				int ret = 0;
+				char *widget = secttok.text + strlen("key-binding:");
+				size_t len = secttok.len - strlen("key-binding:");
 				while (1) {
-					if ((ret = parse_keybinding(&s, config, &tok, errstr)) > 0) {
+					if ((ret = parse_keybinding(&s, &tok, widget, len, press_handler, release_handler, errstr)) > 0) {
 						goto errornomsg;
 					} else if (ret < 0) {
 						start_of_line = 1;
@@ -620,7 +628,7 @@ load_from_text(const char *filename, char *file_contents, size_t len, char **err
 							msg = "Language already set";
 							goto error;
 						}
-						config->keys.mappings[config->keys.mappings_len - 1].lang = ltk_strndup(tok.text, tok.len);
+						config->mappings[config->mappings_len - 1].lang = ltk_strndup(tok.text, tok.len);
 						lang_init = 1;
 					} else if (str_array_equal("map", prev2tok.text, prev2tok.len)) {
 						if (prev1tok.type != STRING || tok.type != STRING) {
@@ -673,14 +681,18 @@ errornomsg:
 }
 
 int
-ltk_config_parsefile(const char *filename, char **errstr) {
+ltk_config_parsefile(
+    const char *filename,
+    keypress_binding_handler press_handler,
+    keyrelease_binding_handler release_handler,
+    char **errstr) {
 	unsigned long len = 0;
 	char *file_contents = ltk_read_file(filename, &len);
 	if (!file_contents) {
 		*errstr = ltk_print_fmt("Unable to open file \"%s\"", filename);
 		return 1;
 	}
-	int ret = load_from_text(filename, file_contents, len, errstr);
+	int ret = load_from_text(filename, file_contents, len, press_handler, release_handler, errstr);
 	ltk_free(file_contents);
 	return ret;
 }
@@ -702,9 +714,18 @@ const char *default_config = "[general]\n"
 
 /* FIXME: improve this configuration */
 int
-ltk_config_load_default(char **errstr) {
+ltk_config_load_default(
+    keypress_binding_handler press_handler,
+    keyrelease_binding_handler release_handler,
+    char **errstr) {
 	char *config_copied = ltk_strdup(default_config);
-	int ret = load_from_text("<default config>", config_copied, strlen(config_copied), errstr);
+	int ret = load_from_text("<default config>", config_copied, strlen(config_copied), press_handler, release_handler, errstr);
 	free(config_copied);
 	return ret;
 }
+
+void
+ltk_keypress_binding_destroy(ltk_keypress_binding b) {
+	ltk_free(b.text);
+	ltk_free(b.rawtext);
+}
diff --git a/src/config.h b/src/config.h
@@ -4,7 +4,6 @@
 #include <stddef.h>
 
 #include "eventdefs.h"
-#include "widget_config.h"
 
 typedef enum{
 	LTK_KEY_BINDING_NOFLAGS = 0,
@@ -14,15 +13,12 @@ typedef enum{
 typedef struct {
 	char *text;
 	char *rawtext;
-	/* FIXME: forward declaration to avoid having to pull everything in for these definitions */
-	ltk_key_callback *callback;
 	ltk_keysym sym;
 	ltk_mod_type mods;
 	ltk_key_binding_flags flags;
 } ltk_keypress_binding;
 
 typedef struct {
-	ltk_key_callback *callback;
 	ltk_keysym sym;
 	ltk_mod_type mods;
 	ltk_key_binding_flags flags;
@@ -39,31 +35,35 @@ typedef struct {
 	size_t mappings_alloc, mappings_len;
 } ltk_language_mapping;
 
-/* FIXME: generic array */
-typedef struct {
-	ltk_keypress_binding *press_bindings;
-	ltk_keyrelease_binding *release_bindings;
-	ltk_language_mapping *mappings;
-	size_t press_alloc, press_len;
-	size_t release_alloc, release_len;
-	size_t mappings_alloc, mappings_len;
-} ltk_keys_config;
-
 typedef struct {
 	char explicit_focus;
 	char all_activatable;
 } ltk_general_config;
 
 typedef struct {
-	ltk_keys_config keys;
+	ltk_language_mapping *mappings;
+	size_t mappings_alloc, mappings_len;
 	ltk_general_config general;
 } ltk_config;
 
+typedef int (*keypress_binding_handler)(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keypress_binding b);
+typedef int (*keyrelease_binding_handler)(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keyrelease_binding b);
+
 void ltk_config_cleanup(void);
 ltk_config *ltk_config_get(void);
 int ltk_config_get_language_index(char *lang, size_t *idx_ret);
 ltk_language_mapping *ltk_config_get_language_mapping(size_t idx);
-int ltk_config_parsefile(const char *filename, char **errstr);
-int ltk_config_load_default(char **errstr);
+int ltk_config_parsefile(
+    const char *filename,
+    keypress_binding_handler press_handler,
+    keyrelease_binding_handler release_handler,
+    char **errstr
+);
+int ltk_config_load_default(
+    keypress_binding_handler press_handler,
+    keyrelease_binding_handler release_handler,
+    char **errstr
+);
+void ltk_keypress_binding_destroy(ltk_keypress_binding b);
 
 #endif /* LTK_CONFIG_H */
diff --git a/src/entry.c b/src/entry.c
@@ -0,0 +1,476 @@
+/*
+ * Copyright (c) 2022-2023 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdarg.h>
+
+#include "proto_types.h"
+#include "event.h"
+#include "memory.h"
+#include "color.h"
+#include "rect.h"
+#include "widget.h"
+#include "ltk.h"
+#include "util.h"
+#include "text.h"
+#include "entry.h"
+#include "graphics.h"
+#include "surface_cache.h"
+#include "theme.h"
+#include "array.h"
+#include "keys.h"
+
+#define MAX_ENTRY_BORDER_WIDTH 100
+#define MAX_ENTRY_PADDING 500
+
+static void ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
+static ltk_entry *ltk_entry_create(ltk_window *window,
+    const char *id, char *text);
+static void ltk_entry_destroy(ltk_widget *self, int shallow);
+static void ltk_entry_redraw_surface(ltk_entry *entry, ltk_surface *s);
+
+static int ltk_entry_key_press(ltk_widget *self, ltk_key_event *event);
+static int ltk_entry_key_release(ltk_widget *self, ltk_key_event *event);
+static int ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event);
+static int ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event);
+static int ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event);
+static int ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event);
+static int ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event);
+
+typedef void (*cb_func)(ltk_entry *, char *, size_t);
+
+/* FIXME: configure mouse actions, e.g. select-word-under-pointer, move-cursor-to-pointer */
+
+static void cursor_to_beginning(ltk_entry *entry, char *text, size_t len);
+static void cursor_to_end(ltk_entry *entry, char *text, size_t len);
+static void cursor_left(ltk_entry *entry, char *text, size_t len);
+static void cursor_right(ltk_entry *entry, char *text, size_t len);
+
+struct key_cb {
+	char *text;
+	cb_func func;
+};
+
+static struct key_cb cb_map[] = {
+	{"cursor-left", &cursor_left},
+	{"cursor-right", &cursor_right},
+	{"cursor-to-beginning", &cursor_to_beginning},
+	{"cursor-to-end", &cursor_to_end},
+};
+
+struct keypress_cfg {
+	ltk_keypress_binding b;
+	struct key_cb cb;
+};
+
+struct keyrelease_cfg {
+	ltk_keyrelease_binding b;
+	struct key_cb cb;
+};
+
+LTK_ARRAY_INIT_DECL_STATIC(keypress, struct keypress_cfg)
+LTK_ARRAY_INIT_IMPL_STATIC(keypress, struct keypress_cfg)
+LTK_ARRAY_INIT_DECL_STATIC(keyrelease, struct keyrelease_cfg)
+LTK_ARRAY_INIT_IMPL_STATIC(keyrelease, struct keyrelease_cfg)
+
+static ltk_array(keypress) *keypresses = NULL;
+static ltk_array(keyrelease) *keyreleases = NULL;
+
+GEN_CB_MAP_HELPERS(cb_map, struct key_cb, text)
+
+int
+ltk_entry_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b) {
+	if (!keypresses)
+		keypresses = ltk_array_create(keypress, 1);
+	struct key_cb *cb = cb_map_get_entry(func_name, func_len);
+	if (!cb)
+		return 1;
+	struct keypress_cfg cfg = {b, *cb};
+	ltk_array_append(keypress, keypresses, cfg);
+	return 0;
+}
+
+int
+ltk_entry_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b) {
+	if (!keyreleases)
+		keyreleases = ltk_array_create(keyrelease, 1);
+	struct key_cb *cb = cb_map_get_entry(func_name, func_len);
+	if (!cb)
+		return 1;
+	struct keyrelease_cfg cfg = {b, *cb};
+	ltk_array_append(keyrelease, keyreleases, cfg);
+	return 0;
+}
+
+static void
+destroy_keypress_cfg(struct keypress_cfg cfg) {
+	ltk_keypress_binding_destroy(cfg.b);
+}
+
+void
+ltk_entry_cleanup(void) {
+	ltk_array_destroy_deep(keypress, keypresses, &destroy_keypress_cfg);
+	ltk_array_destroy(keyrelease, keyreleases);
+	keypresses = NULL;
+	keyreleases = NULL;
+}
+
+static struct ltk_widget_vtable vtable = {
+	.key_press = <k_entry_key_press,
+	.key_release = <k_entry_key_release,
+	.mouse_press = <k_entry_mouse_press,
+	.mouse_release = <k_entry_mouse_release,
+	.release = NULL,
+	.motion_notify = <k_entry_motion_notify,
+	.mouse_leave = <k_entry_mouse_leave,
+	.mouse_enter = <k_entry_mouse_enter,
+	.change_state = NULL,
+	.get_child_at_pos = NULL,
+	.resize = NULL,
+	.hide = NULL,
+	.draw = <k_entry_draw,
+	.destroy = <k_entry_destroy,
+	.child_size_change = NULL,
+	.remove_child = NULL,
+	.type = LTK_WIDGET_ENTRY,
+	.flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_NEEDS_KEYBOARD,
+};
+
+static struct {
+	int border_width;
+	ltk_color text_color;
+	int pad;
+
+	ltk_color border;
+	ltk_color fill;
+
+	ltk_color border_pressed;
+	ltk_color fill_pressed;
+
+	ltk_color border_hover;
+	ltk_color fill_hover;
+
+	ltk_color border_active;
+	ltk_color fill_active;
+
+	ltk_color border_disabled;
+	ltk_color fill_disabled;
+} theme;
+
+/* FIXME:
+need to distinguish between active and focused keybindings - entry binding for opening
+in external text editor should work no matter if active or focused */
+/* FIXME: mouse press also needs to set focused */
+static ltk_theme_parseinfo parseinfo[] = {
+	{"border", THEME_COLOR, {.color = &theme.border}, {.color = "#339999"}, 0, 0, 0},
+	{"border-hover", THEME_COLOR, {.color = &theme.border_hover}, {.color = "#339999"}, 0, 0, 0},
+	{"border-active", THEME_COLOR, {.color = &theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"border-disabled", THEME_COLOR, {.color = &theme.border_disabled}, {.color = "#339999"}, 0, 0, 0},
+	{"border-pressed", THEME_COLOR, {.color = &theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"border-width", THEME_INT, {.i = &theme.border_width}, {.i = 2}, 0, MAX_ENTRY_BORDER_WIDTH, 0},
+	{"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#113355"}, 0, 0, 0},
+	{"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#113355"}, 0, 0, 0},
+	{"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#113355"}, 0, 0, 0},
+	{"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
+	{"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
+	{"pad", THEME_INT, {.i = &theme.pad}, {.i = 5}, 0, MAX_ENTRY_PADDING, 0},
+	{"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0},
+};
+static int parseinfo_sorted = 0;
+
+int
+ltk_entry_ini_handler(ltk_window *window, const char *prop, const char *value) {
+	return ltk_theme_handle_value(window, "entry", prop, value, parseinfo, LENGTH(parseinfo), &parseinfo_sorted);
+}
+
+int
+ltk_entry_fill_theme_defaults(ltk_window *window) {
+	return ltk_theme_fill_defaults(window, "entry", parseinfo, LENGTH(parseinfo));
+}
+
+void
+ltk_entry_uninitialize_theme(ltk_window *window) {
+	ltk_theme_uninitialize(window, parseinfo, LENGTH(parseinfo));
+}
+
+/* FIXME: only keep text in surface to avoid large surface */
+/* -> or maybe not even that? */
+static void
+ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
+	ltk_entry *entry = (ltk_entry *)self;
+	ltk_rect lrect = self->lrect;
+	ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
+	if (clip_final.w <= 0 || clip_final.h <= 0)
+		return;
+	ltk_surface *s;
+	ltk_surface_cache_request_surface_size(entry->key, lrect.w, lrect.h);
+	if (!ltk_surface_cache_get_surface(entry->key, &s) || self->dirty)
+		ltk_entry_redraw_surface(entry, s);
+	ltk_surface_copy(s, draw_surf, clip_final, x + clip_final.x, y + clip_final.y);
+}
+
+static void
+ltk_entry_redraw_surface(ltk_entry *entry, ltk_surface *s) {
+	ltk_rect rect = entry->widget.lrect;
+	int bw = theme.border_width;
+	ltk_color *border = NULL, *fill = NULL;
+	/* FIXME: HOVERACTIVE STATE */
+	if (entry->widget.state & LTK_DISABLED) {
+		border = &theme.border_disabled;
+		fill = &theme.fill_disabled;
+	} else if (entry->widget.state & LTK_PRESSED) {
+		border = &theme.border_pressed;
+		fill = &theme.fill_pressed;
+	} else if (entry->widget.state & LTK_ACTIVE) {
+		border = &theme.border_active;
+		fill = &theme.fill_active;
+	} else if (entry->widget.state & LTK_HOVER) {
+		border = &theme.border_hover;
+		fill = &theme.fill_hover;
+	} else {
+		border = &theme.border;
+		fill = &theme.fill;
+	}
+	rect.x = 0;
+	rect.y = 0;
+	ltk_surface_fill_rect(s, fill, rect);
+	if (bw > 0)
+		ltk_surface_draw_rect(s, border, (ltk_rect){bw / 2, bw / 2, rect.w - bw, rect.h - bw}, bw);
+
+	int text_w, text_h;
+	ltk_text_line_get_size(entry->tl, &text_w, &text_h);
+	/* FIXME: what if text_h > rect.h? */
+	int text_x = bw + theme.pad;
+	int text_y = (rect.h - text_h) / 2;
+	ltk_rect clip_rect = (ltk_rect){text_x, text_y, rect.w - 2 * bw - 2 * theme.pad, text_h};
+	ltk_text_line_draw_clipped(entry->tl, s, &theme.text_color, text_x - entry->cur_offset, text_y, clip_rect);
+	if (entry->widget.state & LTK_FOCUSED) {
+		int x, y, w, h;
+		ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h);
+		/* FIXME: configure line width */
+		ltk_surface_draw_rect(s, &theme.text_color, (ltk_rect){x - entry->cur_offset + text_x, y + text_y, 1, h}, 1);
+	}
+	entry->widget.dirty = 0;
+}
+
+/* FIXME: these don't need len, but they do need rawtext as well */
+static void
+cursor_to_beginning(ltk_entry *entry, char *text, size_t len) {
+	(void)text;
+	(void)len;
+	entry->pos = 0;
+}
+
+static void
+cursor_to_end(ltk_entry *entry, char *text, size_t len) {
+	(void)text;
+	(void)len;
+	entry->pos = entry->len;
+}
+
+/* FIXME: actually move shown text */
+static void
+cursor_left(ltk_entry *entry, char *text, size_t len) {
+	(void)text;
+	(void)len;
+	entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, -1, NULL);
+}
+
+static void
+cursor_right(ltk_entry *entry, char *text, size_t len) {
+	(void)text;
+	(void)len;
+	entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, 1, NULL);
+}
+
+static int
+ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) {
+	ltk_entry *entry = (ltk_entry *)self;
+	ltk_keypress_binding b;
+	for (size_t i = 0; i < ltk_array_length(keypresses); i++) {
+		b = ltk_array_get(keypresses, i).b;
+		/* FIXME: do this properly */
+		if (b.sym == event->sym) {
+			ltk_array_get(keypresses, i).cb.func(entry, event->text, 0);
+			entry->widget.dirty = 1;
+			ltk_window_invalidate_widget_rect(self->window, self);
+			return 1;
+		}
+	}
+	/* FIXME: rawtext? */
+	if (event->text) {
+		/* FIXME: properly handle everything */
+		if (event->text[0] == '\n' || event->text[0] == '\r' || event->text[0] == 0x1b)
+			return 0;
+		size_t len = strlen(event->text);
+		if (entry->alloc < entry->len + len + 1) {
+			size_t new_alloc = ideal_array_size(entry->alloc, entry->len + len + 1);
+			entry->text = ltk_realloc(entry->text, new_alloc);
+			entry->alloc = new_alloc;
+		}
+		memmove(entry->text + entry->pos + len, entry->text + entry->pos, entry->len - entry->pos);
+		memmove(entry->text + entry->pos, event->text, len);
+		entry->len += len;
+		entry->pos += len;
+		entry->text[entry->len] = '\0';
+		ltk_text_line_set_text(entry->tl, entry->text, 0);
+		/* FIXME: need to react to resize and adjust cur_offset */
+		int x, y, w, h;
+		ltk_text_line_get_size(entry->tl, &w, &h);
+		unsigned int ideal_h = h + 2 * theme.border_width + 2 * theme.pad;
+		unsigned int ideal_w = w + 2 * theme.border_width + 2 * theme.pad;
+		if (ideal_w != self->ideal_w || ideal_h != self->ideal_h) {
+			self->ideal_w = ideal_w;
+			self->ideal_h = ideal_h;
+			if (self->parent && self->parent->vtable->child_size_change)
+				self->parent->vtable->child_size_change(self->parent, self);
+		}
+		ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h);
+		/* FIXME: test if anything weird can happen since resize is called by parent->child_size_change,
+		   and then the stuff on the next few lines is done afterwards */
+		/* FIXME: adjustable cursor width */
+		int text_w = entry->widget.lrect.w - 2 * theme.border_width - 2 * theme.pad;
+		if (x - entry->cur_offset + 1 > text_w)
+			entry->cur_offset = x - text_w + 1;
+		entry->widget.dirty = 1;
+		ltk_window_invalidate_widget_rect(self->window, self);
+		return 1;
+	}
+	return 0;
+}
+
+static int
+ltk_entry_key_release(ltk_widget *self, ltk_key_event *event) {
+	(void)self; (void)event;
+	return 0;
+}
+
+static int
+ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event) {
+	(void)self; (void)event;
+	return 0;
+}
+
+static int
+ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event) {
+	(void)self; (void)event;
+	return 0;
+}
+
+static int
+ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event) {
+	(void)self; (void)event;
+	return 0;
+}
+
+static int
+ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event) {
+	(void)self; (void)event;
+	return 0;
+}
+
+static int
+ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event) {
+	(void)self; (void)event;
+	return 0;
+}
+
+static ltk_entry *
+ltk_entry_create(ltk_window *window, const char *id, char *text) {
+	ltk_entry *entry = ltk_malloc(sizeof(ltk_entry));
+
+	uint16_t font_size = window->theme->font_size;
+	entry->tl = ltk_text_line_create(window->text_context, font_size, text, 0, -1);
+	int text_w, text_h;
+	ltk_text_line_get_size(entry->tl, &text_w, &text_h);
+	ltk_fill_widget_defaults(&entry->widget, id, window, &vtable, entry->widget.ideal_w, entry->widget.ideal_h);
+	entry->widget.ideal_w = text_w + theme.border_width * 2 + theme.pad * 2;
+	entry->widget.ideal_h = text_h + theme.border_width * 2 + theme.pad * 2;
+	entry->key = ltk_surface_cache_get_unnamed_key(window->surface_cache, entry->widget.ideal_w, entry->widget.ideal_h);
+	entry->cur_offset = 0;
+	entry->text = ltk_strdup(text);
+	entry->len = strlen(text);
+	entry->alloc = entry->len + 1;
+	entry->pos = entry->sel_start = entry->sel_end = 0;
+	entry->widget.dirty = 1;
+
+	return entry;
+}
+
+static void
+ltk_entry_destroy(ltk_widget *self, int shallow) {
+	(void)shallow;
+	ltk_entry *entry = (ltk_entry *)self;
+	if (!entry) {
+		ltk_warn("Tried to destroy NULL entry.\n");
+		return;
+	}
+	ltk_surface_cache_release_key(entry->key);
+	ltk_text_line_destroy(entry->tl);
+	ltk_free(entry);
+}
+
+/* FIXME: make text optional, command set-text */
+/* entry <entry id> create <text> */
+static int
+ltk_entry_cmd_create(
+    ltk_window *window,
+    char **tokens,
+    size_t num_tokens,
+    ltk_error *err) {
+	ltk_entry *entry;
+	/* FIXME: factor out these repeated pieces */
+	if (num_tokens != 4) {
+		err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
+		err->arg = -1;
+		return 1;
+	}
+	if (!ltk_widget_id_free(tokens[1])) {
+		err->type = ERR_WIDGET_ID_IN_USE;
+		err->arg = 1;
+		return 1;
+	}
+	entry = ltk_entry_create(window, tokens[1], tokens[3]);
+	ltk_set_widget((ltk_widget *)entry, tokens[1]);
+
+	return 0;
+}
+
+/* entry <entry id> <command> ... */
+int
+ltk_entry_cmd(
+    ltk_window *window,
+    char **tokens,
+    size_t num_tokens,
+    ltk_error *err) {
+	if (num_tokens < 3) {
+		err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
+		err->arg = -1;
+		return 1;
+	}
+	if (strcmp(tokens[2], "create") == 0) {
+		return ltk_entry_cmd_create(window, tokens, num_tokens, err);
+	} else {
+		err->type = ERR_INVALID_COMMAND;
+		err->arg = -1;
+		return 1;
+	}
+
+	return 0;
+}
diff --git a/src/entry.h b/src/entry.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2022 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LTK_ENTRY_H
+#define LTK_ENTRY_H
+
+/* Requires the following includes: <X11/Xlib.h>, "rect.h", "widget.h", "ltk.h", "color.h", "text.h" */
+
+#include "err.h"
+#include "config.h"
+
+typedef struct {
+	ltk_widget widget;
+	ltk_text_line *tl;
+	ltk_surface_cache_key *key;
+	char *text;
+	size_t len, alloc, pos, sel_start, sel_end;
+	int cur_offset;
+} ltk_entry;
+
+int ltk_entry_ini_handler(ltk_window *window, const char *prop, const char *value);
+int ltk_entry_fill_theme_defaults(ltk_window *window);
+void ltk_entry_uninitialize_theme(ltk_window *window);
+
+/* FIXME: document that pointers inside binding are taken over! */
+int ltk_entry_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b);
+int ltk_entry_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b);
+void ltk_entry_cleanup(void);
+
+int ltk_entry_cmd(
+    ltk_window *window,
+    char **tokens,
+    size_t num_tokens,
+    ltk_error *
+);
+
+#endif /* LTK_ENTRY_H */
diff --git a/src/event_xlib.c b/src/event_xlib.c
@@ -60,6 +60,8 @@ get_keysym(KeySym sym) {
 	   and I'm not sure how standardized it is */
 	case XK_ISO_Left_Tab: case XK_Tab: return LTK_KEY_TAB;
 	case XK_Escape: return LTK_KEY_ESCAPE;
+	case XK_End: return LTK_KEY_END;
+	case XK_Home: return LTK_KEY_HOME;
 	default: return LTK_KEY_NONE;
 	}
 }
diff --git a/src/eventdefs.h b/src/eventdefs.h
@@ -40,6 +40,8 @@ typedef enum {
 	LTK_KEY_RETURN,
 	LTK_KEY_TAB,
 	LTK_KEY_ESCAPE,
+	LTK_KEY_HOME,
+	LTK_KEY_END
 } ltk_keysym;
 
 typedef enum {
diff --git a/src/keys.h b/src/keys.h
@@ -0,0 +1,60 @@
+#ifndef _KEYS_H_
+#define _KEYS_H_
+
+#include "util.h"
+
+/* FIXME: replace with proper string type */
+struct ltk_search_cmp_helper {
+	const char *text;
+	size_t len;
+};
+
+/* FIXME: documentation */
+#define GEN_CB_MAP_HELPERS(name, typename, cmp_entry)                             \
+                                                                                  \
+static int name##_sorted = 0;                                                     \
+                                                                                  \
+/*                                                                                \
+ * IMPORTANT: The text passed to *_get_entry may not be nul-terminated,           \
+ * so ltk_search_cmp_helper has to be used for the bsearch comparison             \
+ * helper.                                                                        \
+ */                                                                               \
+                                                                                  \
+static int                                                                        \
+name##_search_helper(const void *keyv, const void *entryv) {                      \
+	struct ltk_search_cmp_helper *key = (struct ltk_search_cmp_helper *)keyv; \
+	typename *entry = (typename *)entryv;                                     \
+	int ret = strncmp(key->text, entry->cmp_entry, key->len);                 \
+	if (ret == 0) {                                                           \
+		if (entry->cmp_entry[key->len] == '\0')                           \
+			return 0;                                                 \
+		else                                                              \
+			return -1;                                                \
+	}                                                                         \
+	return ret;                                                               \
+}                                                                                 \
+                                                                                  \
+static int                                                                        \
+name##_sort_helper(const void *entry1v, const void *entry2v) {                    \
+	typename *entry1 = (typename *)entry1v;                                   \
+	typename *entry2 = (typename *)entry2v;                                   \
+	return strcmp(entry1->cmp_entry, entry2->cmp_entry);                      \
+}                                                                                 \
+                                                                                  \
+static typename *                                                                 \
+name##_get_entry(const char *text, size_t len) {                                  \
+	/* just in case */                                                        \
+	if (!name##_sorted) {                                                     \
+		qsort(                                                            \
+		    name, LENGTH(name),                                           \
+		    sizeof(name[0]), &name##_sort_helper);                        \
+		name##_sorted = 1;                                                \
+	}                                                                         \
+	struct ltk_search_cmp_helper tmp = {.len = len, .text = text};            \
+	return bsearch(                                                           \
+	    &tmp, name, LENGTH(name),                                             \
+	    sizeof(name[0]), &name##_search_helper                                \
+	);                                                                        \
+}
+
+#endif
diff --git a/src/ltkd.c b/src/ltkd.c
@@ -53,6 +53,7 @@
 #include "grid.h"
 /* #include "draw.h" */
 #include "button.h"
+#include "entry.h"
 #include "label.h"
 #include "scrollbar.h"
 #include "box.h"
@@ -122,6 +123,7 @@ static void ltk_uninitialize_theme(ltk_window *window);
 static int ltk_ini_handler(void *window, const char *widget, const char *prop, const char *value);
 static int ltk_window_fill_theme_defaults(ltk_window *window);
 static int ltk_window_ini_handler(ltk_window *window, const char *prop, const char *value);
+static void ltk_window_uninitialize_theme(ltk_window *window);
 
 static int read_sock(struct ltk_sock_info *sock);
 static int push_token(struct token_list *tl, char *token);
@@ -144,6 +146,151 @@ static char *sock_path = NULL;
    global originally, but that's just the way it is. */
 static ltk_window *main_window = NULL;
 
+typedef struct {
+	char *name;
+	int (*ini_handler)(ltk_window *, const char *, const char *);
+	int (*fill_theme_defaults)(ltk_window *);
+	void (*uninitialize_theme)(ltk_window *);
+	int (*register_keypress)(const char *, size_t, ltk_keypress_binding);
+	int (*register_keyrelease)(const char *, size_t, ltk_keyrelease_binding);
+	void (*cleanup)(void);
+	int (*cmd)(ltk_window *, char **, size_t, ltk_error *);
+} ltk_widget_funcs;
+
+/* FIXME: use binary search when searching for the widget */
+ltk_widget_funcs widget_funcs[] = {
+	{
+		.name = "box",
+		.ini_handler = NULL,
+		.fill_theme_defaults = NULL,
+		.uninitialize_theme = NULL,
+		.register_keypress = NULL,
+		.register_keyrelease = NULL,
+		.cleanup = NULL,
+		.cmd = <k_box_cmd
+	},
+	{
+		.name = "button",
+		.ini_handler = <k_button_ini_handler,
+		.fill_theme_defaults = <k_button_fill_theme_defaults,
+		.uninitialize_theme = <k_button_uninitialize_theme,
+		.register_keypress = NULL,
+		.register_keyrelease = NULL,
+		.cleanup = NULL,
+		.cmd = <k_button_cmd
+	},
+	{
+		.name = "entry",
+		.ini_handler = <k_entry_ini_handler,
+		.fill_theme_defaults = <k_entry_fill_theme_defaults,
+		.uninitialize_theme = <k_entry_uninitialize_theme,
+		.register_keypress = <k_entry_register_keypress,
+		.register_keyrelease = <k_entry_register_keyrelease,
+		.cleanup = <k_entry_cleanup,
+		.cmd = <k_entry_cmd
+	},
+	{
+		.name = "grid",
+		.ini_handler = NULL,
+		.fill_theme_defaults = NULL,
+		.uninitialize_theme = NULL,
+		.register_keypress = NULL,
+		.register_keyrelease = NULL,
+		.cleanup = NULL,
+		.cmd = <k_grid_cmd
+	},
+	{
+		.name = "label",
+		.ini_handler = <k_label_ini_handler,
+		.fill_theme_defaults = <k_label_fill_theme_defaults,
+		.uninitialize_theme = <k_label_uninitialize_theme,
+		.register_keypress = NULL,
+		.register_keyrelease = NULL,
+		.cleanup = NULL,
+		.cmd = <k_label_cmd
+	},
+	{
+		.name = "menu",
+		.ini_handler = <k_menu_ini_handler,
+		.fill_theme_defaults = <k_menu_fill_theme_defaults,
+		.uninitialize_theme = <k_menu_uninitialize_theme,
+		.register_keypress = NULL,
+		.register_keyrelease = NULL,
+		.cleanup = NULL,
+		.cmd = <k_menu_cmd
+	},
+	{
+		.name = "menuentry",
+		.ini_handler = <k_menuentry_ini_handler,
+		.fill_theme_defaults = <k_menuentry_fill_theme_defaults,
+		.uninitialize_theme = <k_menuentry_uninitialize_theme,
+		.register_keypress = NULL,
+		.register_keyrelease = NULL,
+		.cleanup = NULL,
+		.cmd = <k_menuentry_cmd
+	},
+	{
+		.name = "submenu",
+		.ini_handler = <k_submenu_ini_handler,
+		.fill_theme_defaults = <k_submenu_fill_theme_defaults,
+		.uninitialize_theme = <k_submenu_uninitialize_theme,
+		.register_keypress = NULL,
+		.register_keyrelease = NULL,
+		.cleanup = NULL,
+		.cmd = <k_menu_cmd
+	},
+	{
+		.name = "submenuentry",
+		.ini_handler = <k_submenuentry_ini_handler,
+		.fill_theme_defaults = <k_submenuentry_fill_theme_defaults,
+		.uninitialize_theme = <k_submenuentry_uninitialize_theme,
+		.register_keypress = NULL,
+		.register_keyrelease = NULL,
+		.cleanup = NULL,
+		/* This "widget" is only needed to have separate styles for regular
+		   menu entries and submenu entries. "submenu" is just an alias for
+		   "menu" in most cases - it's just needed when creating a menu to
+		   decide if it's a submenu or not.
+		   FIXME: is that even necessary? Why can't it just decide if it's
+		   a submenu based on whether it has a parent or not?
+		   -> I guess right-click menus are also just submenus, so they
+		   need to set it explicitly, but wasn't there another reaseon? */
+		.cmd = NULL
+	},
+	{
+		.name = "scrollbar",
+		.ini_handler = <k_scrollbar_ini_handler,
+		.fill_theme_defaults = <k_scrollbar_fill_theme_defaults,
+		.uninitialize_theme = <k_scrollbar_uninitialize_theme,
+		.register_keypress = NULL,
+		.register_keyrelease = NULL,
+		.cleanup = NULL,
+		.cmd = NULL
+	},
+	{
+		/* Handler for general widget key bindings. */
+		.name = "widget",
+		.ini_handler = NULL,
+		.fill_theme_defaults = NULL,
+		.uninitialize_theme = NULL,
+		.register_keypress = <k_widget_register_keypress,
+		.register_keyrelease = <k_widget_register_keyrelease,
+		.cleanup = <k_widget_cleanup,
+		.cmd = NULL
+	},
+	{
+		/* Handler for window theme. */
+		.name = "window",
+		.ini_handler = <k_window_ini_handler,
+		.fill_theme_defaults = <k_window_fill_theme_defaults,
+		.uninitialize_theme = <k_window_uninitialize_theme,
+		.register_keypress = NULL,
+		.register_keyrelease = NULL,
+		.cleanup = NULL,
+		.cmd = NULL
+	}
+};
+
 int
 main(int argc, char *argv[]) {
 	setlocale(LC_CTYPE, "");
@@ -168,6 +315,7 @@ main(int argc, char *argv[]) {
 	ltk_logfile = open_log(ltk_dir);
 	if (!ltk_logfile) ltk_fatal_errno("Unable to open log file.\n");
 
+	/* FIXME: move to widget_funcs? */
 	ltk_widgets_init();
 
 	/* FIXME: set window size properly - I only run it in a tiling WM
@@ -490,7 +638,10 @@ ltk_cleanup(void) {
 	}
 
 	ltk_config_cleanup();
-	ltk_widgets_cleanup();
+	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+		if (widget_funcs[i].cleanup)
+			widget_funcs[i].cleanup();
+	}
 	ltk_events_cleanup();
 	if (main_window) {
 		ltk_uninitialize_theme(main_window);
@@ -813,30 +964,23 @@ ltk_window_ini_handler(ltk_window *window, const char *prop, const char *value) 
 	return ltk_theme_handle_value(window, "window", prop, value, theme_parseinfo, LENGTH(theme_parseinfo), &theme_parseinfo_sorted);
 }
 
+static void
+ltk_window_uninitialize_theme(ltk_window *window) {
+	ltk_theme_uninitialize(window, theme_parseinfo, LENGTH(theme_parseinfo));
+}
+
 /* FIXME: standardize return codes - usually, 0 is returned on success, but ini.h
    uses 1 on success, so this is all a bit confusing */
+/* FIXME: switch away from ini.h */
 static int
 ltk_ini_handler(void *window, const char *widget, const char *prop, const char *value) {
-	if (strcmp(widget, "window") == 0) {
-		ltk_window_ini_handler(window, prop, value);
-	} else if (strcmp(widget, "button") == 0) {
-		ltk_button_ini_handler(window, prop, value);
-	} else if (strcmp(widget, "label") == 0) {
-		ltk_label_ini_handler(window, prop, value);
-	} else if (strcmp(widget, "scrollbar") == 0) {
-		ltk_scrollbar_ini_handler(window, prop, value);
-	} else if (strcmp(widget, "menu") == 0) {
-		ltk_menu_ini_handler(window, prop, value);
-	} else if (strcmp(widget, "submenu") == 0) {
-		ltk_submenu_ini_handler(window, prop, value);
-	} else if (strcmp(widget, "menuentry") == 0) {
-		ltk_menuentry_ini_handler(window, prop, value);
-	} else if (strcmp(widget, "submenuentry") == 0) {
-		ltk_menuentry_ini_handler(window, prop, value);
-	} else {
-		return 0;
+	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+		if (widget_funcs[i].ini_handler && !strcmp(widget, widget_funcs[i].name)) {
+			widget_funcs[i].ini_handler(window, prop, value);
+			return 1;
+		}
 	}
-	return 1;
+	return 0;
 }
 
 static void
@@ -845,29 +989,46 @@ ltk_load_theme(ltk_window *window, const char *path) {
 	if (ini_parse(path, ltk_ini_handler, window) != 0) {
 		ltk_warn("Unable to load theme.\n");
 	}
-	if (ltk_window_fill_theme_defaults(window)    ||
-	    ltk_button_fill_theme_defaults(window)    ||
-	    ltk_label_fill_theme_defaults(window)     ||
-	    ltk_scrollbar_fill_theme_defaults(window) ||
-	    ltk_menu_fill_theme_defaults(window)      ||
-	    ltk_submenu_fill_theme_defaults(window)   ||
-	    ltk_menuentry_fill_theme_defaults(window) ||
-	    ltk_submenuentry_fill_theme_defaults(window)) {
-		ltk_uninitialize_theme(window);
-		ltk_fatal("Unable to load theme defaults.\n");
+	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+		if (widget_funcs[i].fill_theme_defaults) {
+			if (widget_funcs[i].fill_theme_defaults(window)) {
+				ltk_uninitialize_theme(window);
+				ltk_fatal("Unable to load theme defaults.\n");
+			}
+		}
 	}
 }
 
 static void
 ltk_uninitialize_theme(ltk_window *window) {
-	ltk_theme_uninitialize(window, theme_parseinfo, LENGTH(theme_parseinfo));
-	ltk_button_uninitialize_theme(window);
-	ltk_label_uninitialize_theme(window);
-	ltk_scrollbar_uninitialize_theme(window);
-	ltk_menu_uninitialize_theme(window);
-	ltk_submenu_uninitialize_theme(window);
-	ltk_menuentry_uninitialize_theme(window);
-	ltk_submenuentry_uninitialize_theme(window);
+	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+		if (widget_funcs[i].uninitialize_theme)
+			widget_funcs[i].uninitialize_theme(window);
+	}
+}
+
+static int
+handle_keypress_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keypress_binding b) {
+	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+		if (str_array_equal(widget_funcs[i].name, widget_name, wlen)) {
+			if (!widget_funcs[i].register_keypress)
+				return 1;
+			return widget_funcs[i].register_keypress(name, nlen, b);
+		}
+	}
+	return 1;
+}
+
+static int
+handle_keyrelease_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keyrelease_binding b) {
+	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+		if (str_array_equal(widget_funcs[i].name, widget_name, wlen)) {
+			if (!widget_funcs[i].register_keyrelease)
+				return 1;
+			return widget_funcs[i].register_keyrelease(name, nlen, b);
+		}
+	}
+	return 1;
 }
 
 static ltk_window *
@@ -886,12 +1047,12 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
 	/* FIXME: search different directories for config */
 	char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg");
 	char *errstr = NULL;
-	if (ltk_config_parsefile(config_path, &errstr)) {
+	if (ltk_config_parsefile(config_path, &handle_keypress_binding, &handle_keyrelease_binding, &errstr)) {
 		if (errstr) {
 			ltk_warn("Unable to load config: %s\n", errstr);
 			ltk_free(errstr);
 		}
-		if (ltk_config_load_default(&errstr)) {
+		if (ltk_config_load_default(&handle_keypress_binding, &handle_keyrelease_binding, &errstr)) {
 			/* FIXME: I guess errstr isn't freed here, but whatever */
 			ltk_fatal("Unable to load default config: %s\n", errstr);
 		}
@@ -925,7 +1086,7 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
 	window->dirty_rect.y = 0;
 	window->surface = ltk_surface_from_window(window->renderdata, w, h);
 
-	window->text_context = ltk_text_context_create(window, window->theme->font);
+	window->text_context = ltk_text_context_create(window->renderdata, window->theme->font);
 
 	return window;
 }
@@ -1497,20 +1658,6 @@ process_commands(ltk_window *window, int client) {
 				errdetail.arg = -1;
 				err = 1;
 				seq = sock->last_seq;
-			} else if (strcmp(tokens[0], "grid") == 0) {
-				err = ltk_grid_cmd(window, tokens, num_tokens, &errdetail);
-			} else if (strcmp(tokens[0], "box") == 0) {
-				err = ltk_box_cmd(window, tokens, num_tokens, &errdetail);
-			} else if (strcmp(tokens[0], "button") == 0) {
-				err = ltk_button_cmd(window, tokens, num_tokens, &errdetail);
-			} else if (strcmp(tokens[0], "label") == 0) {
-				err = ltk_label_cmd(window, tokens, num_tokens, &errdetail);
-			} else if (strcmp(tokens[0], "menu") == 0) {
-				err = ltk_menu_cmd(window, tokens, num_tokens, &errdetail);
-			} else if (strcmp(tokens[0], "submenu") == 0) {
-				err = ltk_menu_cmd(window, tokens, num_tokens, &errdetail);
-			} else if (strcmp(tokens[0], "menuentry") == 0) {
-				err = ltk_menuentry_cmd(window, tokens, num_tokens, &errdetail);
 			} else if (strcmp(tokens[0], "set-root-widget") == 0) {
 				err = ltk_set_root_widget_cmd(window, tokens, num_tokens, &errdetail);
 			} else if (strcmp(tokens[0], "quit") == 0) {
@@ -1537,9 +1684,18 @@ process_commands(ltk_window *window, int client) {
 				}
 				last = 1;
 			} else {
-				errdetail.type = ERR_INVALID_COMMAND;
-				errdetail.arg = -1;
-				err = 1;
+				int found = 0;
+				for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
+					if (widget_funcs[i].cmd && !strcmp(tokens[0], widget_funcs[i].name)) {
+						err = widget_funcs[i].cmd(window, tokens, num_tokens, &errdetail);
+						found = 1;
+					}
+				}
+				if (!found) {
+					errdetail.type = ERR_INVALID_COMMAND;
+					errdetail.arg = -1;
+					err = 1;
+				}
 			}
 			sock->tokens.num_tokens = 0;
 			sock->last_seq = seq;
diff --git a/src/memory.c b/src/memory.c
@@ -147,6 +147,7 @@ ideal_array_size(size_t old, size_t needed) {
 	/* FIXME: the shrinking here only makes sense if not
 	   many elements are removed at once - what would be
 	   more sensible here? */
+	/* FIXME: overflow */
 	if (old < needed)
 		ret = old * 2 > needed ? old * 2 : needed;
 	else if (needed * 4 < old)
diff --git a/src/proto_types.h b/src/proto_types.h
@@ -9,7 +9,8 @@
 #define LTK_WIDGET_BOX       5
 #define LTK_WIDGET_MENU      6
 #define LTK_WIDGET_MENUENTRY 7
-#define LTK_NUM_WIDGETS      8
+#define LTK_WIDGET_ENTRY     8
+#define LTK_NUM_WIDGETS      9
 
 #define LTK_WIDGETMASK_UNKNOWN    (UINT32_C(1) << LTK_WIDGET_UNKNOWN)
 #define LTK_WIDGETMASK_ANY        (UINT32_C(0xFFFF))
@@ -19,6 +20,7 @@
 #define LTK_WIDGETMASK_BOX        (UINT32_C(1) << LTK_WIDGET_BOX)
 #define LTK_WIDGETMASK_MENU       (UINT32_C(1) << LTK_WIDGET_MENU)
 #define LTK_WIDGETMASK_MENUENTRY  (UINT32_C(1) << LTK_WIDGET_MENUENTRY)
+#define LTK_WIDGETMASK_ENTRY      (UINT32_C(1) << LTK_WIDGET_ENTRY)
 
 /* P == protocol; W == widget */
 
diff --git a/src/text.h b/src/text.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org>
+ * Copyright (c) 2021-2023 lumidify <nobody@lumidify.org>
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -14,8 +14,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#ifndef _LTK_TEXT_H_
-#define _LTK_TEXT_H_
+#ifndef LTK_TEXT_H
+#define LTK_TEXT_H
 
 #include "color.h"
 #include "graphics.h"
@@ -24,10 +24,7 @@
 typedef struct ltk_text_line ltk_text_line;
 /* typedef struct ltk_text_context ltk_text_context; */
 
-/* FIXME: something to hold display, etc. to avoid circular reference between window and text context */
-/* FIXME: this is done now (ltk_renderdata), just need to use it here... */
-
-ltk_text_context *ltk_text_context_create(ltk_window *window, char *default_font);
+ltk_text_context *ltk_text_context_create(ltk_renderdata *data, char *default_font);
 void ltk_text_context_destroy(ltk_text_context *ctx);
 
 /* FIXME: allow to give length of text */
@@ -36,29 +33,48 @@ ltk_text_line *ltk_text_line_create(ltk_text_context *ctx, uint16_t font_size, c
 void ltk_text_line_set_width(ltk_text_line *tl, int width);
 void ltk_text_line_get_size(ltk_text_line *tl, int *w, int *h);
 void ltk_text_line_destroy(ltk_text_line *tl);
+/* FIXME: length of text */
+void ltk_text_line_set_text(ltk_text_line *line, char *text, int take_over_text);
 
 /* Draw the entire line to a surface. */
 /* FIXME: Some widgets rely on this to not fail when negative coordinates are given or
    the text goes outside of the surface boundaries - in the stb backend, this is taken
    into account and the pango-xft backend doesn't *seem* to have any problems with it,
-   but I don't know if that's guaranteed. Proper clipping would be better, but Pango
-   can't do that. */
+   but I don't know if that's guaranteed. */
 void ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, int y);
 
-/* Get the smallest rectangle of the line that can be drawn while covering 'clip'.
- * The Pango backend will currently always return the full rectangle for the line
- * because it doesn't support clipping. Other backends may just return a slightly
- * larger rectangle so they can draw full glyphs. */
-ltk_rect ltk_text_line_get_minimal_clip_rect(ltk_text_line *tl, ltk_rect clip);
-
-/* Draw a line onto a surface at position x,y and clipped to 'clip'. Note that
- * clipping isn't supported properly, so the drawn part of the line may be
- * larger than 'clip'. In order to find out the exact size of the drawn section,
- * use ltk_rect_line_get_minimal_clip_rect. */
+/* Draw a line onto a surface at position x,y and clipped to 'clip'.
+   Note that 'clip' is relative to the origin of the given surface, not 'x' and 'y'. */
 void ltk_text_line_draw_clipped(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, int y, ltk_rect clip);
 
-/* FIXME: Any way to implement clipping properly? The text would need to be drawn
-   to an intermediate surface, but then we lose alpha blending with the background
-   (unless another library like cairo is used, which I want to avoid). */
+void ltk_text_line_clear_attrs(ltk_text_line *tl);
+void ltk_text_line_add_attr_bg(ltk_text_line *tl, size_t start, size_t end, ltk_color *color);
+void ltk_text_line_add_attr_fg(ltk_text_line *tl, size_t start, size_t end, ltk_color *color);
+
+typedef enum {
+	LTK_TEXT_LTR,
+	LTK_TEXT_RTL,
+} ltk_text_direction;
+
+/* FIXME: support for strong and weak cursor */
+/* FIXME: better interface that doesn't just return ltr on error (e.g. invalid index) */
+ltk_text_direction ltk_text_line_get_byte_direction(ltk_text_line *tl, size_t byte);
+ltk_text_direction ltk_text_line_get_softline_direction(ltk_text_line *tl, size_t line);
+size_t ltk_text_line_get_num_softlines(ltk_text_line *tl);
+size_t ltk_text_line_xy_to_pos(ltk_text_line *tl, int x, int y, int snap_nearest);
+void ltk_text_line_pos_to_rect(ltk_text_line *tl, size_t pos, int *x_ret, int *y_ret, int *w_ret, int *h_ret);
+size_t ltk_text_line_x_softline_to_pos(ltk_text_line *tl, int x, size_t softline, int snap_nearest);
+void ltk_text_line_pos_to_x_softline(ltk_text_line *tl, size_t pos, int middle, int *x_ret, size_t *softline_ret);
+size_t ltk_text_line_move_cursor_visually(ltk_text_line *tl, size_t pos, int movement, size_t *prev_index_ret);
+
+/* FIXME: two versions: bigline and normal line with text stored in memory but no bidi etc. */
+/* need to set endline separator so it can return when end of line reached! */
+/* FIXME: does this even make sense? */
+void ltk_text_bline_create(
+    size_t (*iter_cur)(void *data),
+    void (*iter_next)(void *data, char **text_ret, size_t *len_ret),
+    void (*iter_prev)(void *data, size_t maxlen, char **text_ret, size_t *len_ret),
+    void *data
+);
 
-#endif /* _LTK_TEXT_H_ */
+#endif /* LTK_TEXT_H */
diff --git a/src/text_pango.c b/src/text_pango.c
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org>
+ * Copyright (c) 2021-2023 lumidify <nobody@lumidify.org>
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -25,6 +25,7 @@
 #include <X11/Xutil.h>
 
 #include <pango/pangoxft.h>
+#include <pango/pango-utils.h> /* for PANGO_VERSION_CHECK */
 
 #include "xlib_shared.h"
 #include "memory.h"
@@ -38,22 +39,45 @@
 struct ltk_text_line {
 	ltk_text_context *ctx;
 	char *text;
+	size_t len;
 	PangoLayout *layout;
 	uint16_t font_size;
+	PangoAttrList *attrs;
 };
 
 struct ltk_text_context {
-	ltk_window *window;
+	ltk_renderdata *data;
 	PangoFontMap *fontmap;
 	PangoContext *context;
 	char *default_font;
 };
 
+size_t
+prev_utf8(char *text, size_t index) {
+	if (index == 0)
+		return 0;
+	size_t i = index - 1;
+	/* find valid utf8 char - this probably needs to be improved */
+	while (i > 0 && ((text[i] & 0xC0) == 0x80))
+		i--;
+	return i;
+}
+
+size_t
+next_utf8(char *text, size_t len, size_t index) {
+	if (index >= len)
+		return len;
+	size_t i = index + 1;
+	while (i < len && ((text[i] & 0xC0) == 0x80))
+		i++;
+	return i;
+}
+
 ltk_text_context *
-ltk_text_context_create(ltk_window *window, char *default_font) {
+ltk_text_context_create(ltk_renderdata *data, char *default_font) {
 	ltk_text_context *ctx = ltk_malloc(sizeof(ltk_text_context));
-	ctx->window = window;
-	ctx->fontmap = pango_xft_get_font_map(window->renderdata->dpy, window->renderdata->screen);
+	ctx->data = data;
+	ctx->fontmap = pango_xft_get_font_map(data->dpy, data->screen);
 	ctx->context = pango_font_map_create_context(ctx->fontmap);
 	ctx->default_font = ltk_strdup(default_font);
 	return ctx;
@@ -71,7 +95,22 @@ ltk_text_context_destroy(ltk_text_context *ctx) {
 
 void
 ltk_text_line_set_width(ltk_text_line *tl, int width) {
-	pango_layout_set_width(tl->layout, width * PANGO_SCALE);
+	if (width <= 0)
+		pango_layout_set_width(tl->layout, -1);
+	else
+		pango_layout_set_width(tl->layout, width * PANGO_SCALE);
+}
+
+void
+ltk_text_line_set_text(ltk_text_line *tl, char *text, int take_over_text) {
+	if (tl->text)
+		free(tl->text);
+	if (take_over_text)
+		tl->text = text;
+	else
+		tl->text = ltk_strdup(text);
+	tl->len = strlen(tl->text);
+	pango_layout_set_text(tl->layout, tl->text, tl->len);
 }
 
 ltk_text_line *
@@ -81,6 +120,7 @@ ltk_text_line_create(ltk_text_context *ctx, uint16_t font_size, char *text, int 
 		tl->text = text;
 	else
 		tl->text = ltk_strdup(text);
+	tl->len = strlen(tl->text);
 	tl->font_size = font_size;
 	tl->layout = pango_layout_new(ctx->context);
 
@@ -91,7 +131,10 @@ ltk_text_line_create(ltk_text_context *ctx, uint16_t font_size, char *text, int 
 	tl->ctx = ctx;
 	pango_layout_set_wrap(tl->layout, PANGO_WRAP_WORD_CHAR);
 	pango_layout_set_text(tl->layout, text, -1);
-	ltk_text_line_set_width(tl, width * PANGO_SCALE);
+	if (width > 0)
+		ltk_text_line_set_width(tl, width * PANGO_SCALE);
+	tl->attrs = NULL;
+	ltk_text_line_clear_attrs(tl);
 
 	return tl;
 }
@@ -102,19 +145,22 @@ ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, i
 	pango_xft_render_layout(d, &color->xftcolor, tl->layout, x * PANGO_SCALE, y * PANGO_SCALE);
 }
 
-/* FIXME: any way to actually implement clipping with pango? */
 void
 ltk_text_line_draw_clipped(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, int y, ltk_rect clip) {
-	(void)clip;
+	XftDraw *d = ltk_surface_get_xft_draw(s);
+	/* FIXME: check for integer overflow */
+	XPoint points[] = {
+	    {clip.x, clip.y},
+	    {clip.x + clip.w, clip.y},
+	    {clip.x + clip.w, clip.y + clip.h},
+	    {clip.x, clip.y + clip.h}
+	};
+	Region r = XPolygonRegion(points, 4, EvenOddRule);
+	/* FIXME: error checking */
+	XftDrawSetClip(d, r);
 	ltk_text_line_draw(tl, s, color, x, y);
-}
-
-ltk_rect
-ltk_text_line_get_minimal_clip_rect(ltk_text_line *tl, ltk_rect clip) {
-	(void)clip;
-	int w, h;
-	ltk_text_line_get_size(tl, &w, &h);
-	return (ltk_rect){0, 0, w, h};
+	XDestroyRegion(r);
+	XftDrawSetClip(d, NULL);
 }
 
 void
@@ -123,7 +169,214 @@ ltk_text_line_get_size(ltk_text_line *tl, int *w, int *h) {
 }
 
 void
+ltk_text_line_clear_attrs(ltk_text_line *tl) {
+	PangoAttrList *attrs = pango_attr_list_new();
+	#if PANGO_VERSION_CHECK(1, 44, 0)
+	PangoAttribute *no_hyphens = pango_attr_insert_hyphens_new(FALSE);
+	pango_attr_list_insert(attrs, no_hyphens);
+	#endif
+	pango_layout_set_attributes(tl->layout, attrs);
+	if (tl->attrs)
+		pango_attr_list_unref(tl->attrs);
+	tl->attrs = attrs;
+}
+
+void
+ltk_text_line_add_attr_bg(ltk_text_line *tl, size_t start, size_t end, ltk_color *color) {
+	XRenderColor c = color->xftcolor.color;
+	PangoAttribute *attr = pango_attr_background_new(c.red, c.green, c.blue);
+	attr->start_index = start;
+	attr->end_index = end;
+	pango_attr_list_insert(tl->attrs, attr);
+	pango_layout_set_attributes(tl->layout, tl->attrs);
+}
+
+void
+ltk_text_line_add_attr_fg(ltk_text_line *tl, size_t start, size_t end, ltk_color *color) {
+	XRenderColor c = color->xftcolor.color;
+	PangoAttribute *attr = pango_attr_foreground_new(c.red, c.green, c.blue);
+	attr->start_index = start;
+	attr->end_index = end;
+	pango_attr_list_insert(tl->attrs, attr);
+	pango_layout_set_attributes(tl->layout, tl->attrs);
+}
+
+ltk_text_direction
+ltk_text_line_get_softline_direction(ltk_text_line *tl, size_t line) {
+	int num_softlines = pango_layout_get_line_count(tl->layout);
+	if ((int)line >= num_softlines)
+		return LTK_TEXT_LTR;
+	PangoLayoutLine *sl = pango_layout_get_line_readonly(tl->layout, (int)line);
+	if (!sl)
+		return LTK_TEXT_LTR;
+	return sl->resolved_dir == PANGO_DIRECTION_RTL || sl->resolved_dir == PANGO_DIRECTION_WEAK_RTL ? LTK_TEXT_RTL : LTK_TEXT_LTR;
+}
+
+ltk_text_direction
+ltk_text_line_get_byte_direction(ltk_text_line *tl, size_t byte) {
+	/* FIXME: check if index out of range first? */
+	PangoDirection dir = pango_layout_get_direction(tl->layout, (int)byte);
+	return dir == PANGO_DIRECTION_RTL || dir == PANGO_DIRECTION_WEAK_RTL ? LTK_TEXT_RTL : LTK_TEXT_LTR;
+}
+
+size_t
+ltk_text_line_get_num_softlines(ltk_text_line *tl) {
+	return (size_t)pango_layout_get_line_count(tl->layout);
+}
+
+size_t
+ltk_text_line_xy_to_pos(ltk_text_line *tl, int x, int y, int snap_nearest) {
+	int index, trailing;
+	pango_layout_xy_to_index(
+	    tl->layout,
+	    x * PANGO_SCALE, y * PANGO_SCALE,
+	    &index, & trailing
+	);
+	if (snap_nearest) {
+		while (trailing > 0) {
+			trailing--;
+			/* FIXME: proper string type with length */
+			// FIXME: next_utf8 should be size_t
+			index = next_utf8(tl->text, tl->len, index);
+		}
+	}
+	return (size_t)index;
+}
+
+/* FIXME: get_nearest_legal_pos */
+/* FIXME: factor out common text code from ltk and ledit */
+
+/* WARNING: width can be negative - https://docs.gtk.org/Pango/method.Layout.index_to_pos.html */
+void
+ltk_text_line_pos_to_rect(ltk_text_line *tl, size_t pos, int *x_ret, int *y_ret, int *w_ret, int *h_ret) {
+	PangoRectangle rect;
+	pango_layout_index_to_pos(tl->layout, (int)pos, &rect);
+	*x_ret = rect.x / PANGO_SCALE;
+	*y_ret = rect.y / PANGO_SCALE;
+	*w_ret = rect.width / PANGO_SCALE;
+	*h_ret = rect.height / PANGO_SCALE;
+}
+
+/* FIXME: a lot more error checking, including integer overflows */
+size_t
+ltk_text_line_x_softline_to_pos(ltk_text_line *tl, int x, size_t softline, int snap_nearest) {
+	int trailing = 0;
+	int x_relative = x * PANGO_SCALE;
+	PangoLayoutLine *pango_line = pango_layout_get_line_readonly(tl->layout, softline);
+	int tlw, tlh;
+	pango_layout_get_size(tl->layout, &tlw, &tlh);
+	/* x is absolute, so the margin at the left needs to be subtracted */
+	if (pango_line->resolved_dir == PANGO_DIRECTION_RTL) {
+		PangoRectangle rect;
+		pango_layout_line_get_extents(pango_line, NULL, &rect);
+		x_relative -= (tlw - rect.width);
+	}
+	int tmp_pos;
+	pango_layout_line_x_to_index(
+	    pango_line, x_relative, &tmp_pos, &trailing
+	);
+	size_t pos = (size_t)tmp_pos;
+	/* snap to the nearest border between graphemes */
+	if (snap_nearest) {
+		while (trailing > 0) {
+			trailing--;
+			pos = next_utf8(tl->text, tl->len, pos);
+		}
+	}
+	return pos;
+}
+
+void
+ltk_text_line_pos_to_x_softline(ltk_text_line *tl, size_t pos, int middle, int *x_ret, size_t *softline_ret) {
+	/*
+	if (pos > INT_MAX)
+		err_overflow();
+	*/
+	int sl_tmp;
+	pango_layout_index_to_line_x(tl->layout, (int)pos, 0, &sl_tmp, x_ret);
+	*softline_ret = sl_tmp;
+	PangoLayoutLine *pango_line = pango_layout_get_line_readonly(tl->layout, *softline_ret);
+	int tlw, tlh;
+	pango_layout_get_size(tl->layout, &tlw, &tlh);
+	/* FIXME: wouldn't it be easier to just use pango_layout_index_to_pos for everything here? */
+	/* add left margin to x position if line is aligned right */
+	if (pango_line->resolved_dir == PANGO_DIRECTION_RTL) {
+		PangoRectangle rect;
+		pango_layout_line_get_extents(pango_line, NULL, &rect);
+		*x_ret += (tlw - rect.width);
+	}
+	if (middle) {
+		PangoRectangle rect;
+		pango_layout_index_to_pos(tl->layout, (int)pos, &rect);
+		*x_ret += rect.width / 2;
+	}
+	*x_ret /= PANGO_SCALE; /* FIXME: PANGO_PIXELS, etc. */
+}
+
+/* prev_index_ret is used instead of just calling get_legal_normal_pos
+   because weird things happen otherwise
+   -> in certain cases, this is still weird because prev_index_ret sometimes
+      is not at the end of the line, but this is the best I could come up
+      with for now */
+size_t
+ltk_text_line_move_cursor_visually(ltk_text_line *tl, size_t pos, int movement, size_t *prev_index_ret) {
+	/* FIXME
+	if (pos > INT_MAX)
+		err_overflow();
+	*/
+	/* FIXME: trailing */
+	int trailing = 0;
+	int tmp_index;
+	int new_index = (int)pos, last_index = (int)pos;
+	int dir = 1;
+	int num = movement;
+	if (movement < 0) {
+		dir = -1;
+		num = -movement;
+	}
+	/* FIXME: This is stupid. Anything outside the range of int won't work
+	   anyways because of pango (and because everything else would break
+	   anyways with such long lines), so it's stupid to do all this weird
+	   casting. */
+	/*
+	if (cur_line->len > INT_MAX)
+		err_overflow();
+	*/
+	while (num > 0) {
+		tmp_index = new_index;
+		pango_layout_move_cursor_visually(
+		    tl->layout, TRUE,
+		    new_index, trailing, dir,
+		    &new_index, &trailing
+		);
+		/* for some reason, this is necessary */
+		if (new_index < 0)
+			new_index = 0;
+		else if (new_index > (int)tl->len)
+			new_index = (int)tl->len;
+		num--;
+		if (tmp_index != new_index)
+			last_index = tmp_index;
+	}
+	/* FIXME: Allow cursor to be at end of soft line */
+	/* 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 */
+	/* FIXME: spaces at end of softlines are weird in normal mode */
+	while (trailing > 0) {
+		trailing--;
+		new_index = next_utf8(tl->text, tl->len, new_index);
+	}
+	if (new_index < 0)
+		new_index = 0;
+	if (prev_index_ret)
+		*prev_index_ret = (size_t)last_index;
+	return (size_t)new_index;
+}
+
+void
 ltk_text_line_destroy(ltk_text_line *tl) {
+	if (tl->attrs)
+		g_object_unref(tl->attrs);
 	g_object_unref(tl->layout);
 	ltk_free(tl->text);
 	ltk_free(tl);
diff --git a/src/text_stb.c b/src/text_stb.c
@@ -96,7 +96,7 @@ KHASH_MAP_INIT_INT(glyphinfo, ltk_glyph_info*)
 KHASH_MAP_INIT_INT(glyphcache, khash_t(glyphinfo)*)
 
 struct ltk_text_context {
-	ltk_window *window;
+	ltk_renderdata *data;
 	khash_t(glyphcache) *glyph_cache;
 	ltk_font **fonts;
 	int num_fonts;
@@ -202,9 +202,9 @@ static size_t u8_wc_toutf8(char *dest, uint32_t ch) {
 */
 
 ltk_text_context *
-ltk_text_context_create(ltk_window *window, char *default_font) {
+ltk_text_context_create(ltk_renderdata *data, char *default_font) {
 	ltk_text_context *ctx = ltk_malloc(sizeof(ltk_text_context));
-	ctx->window = window;
+	ctx->data = data;
 	ctx->glyph_cache = kh_init(glyphcache);
 	ctx->fonts = ltk_malloc(sizeof(ltk_font *));
 	ctx->num_fonts = 0;
@@ -228,6 +228,8 @@ ltk_text_context_destroy(ltk_text_context *ctx) {
 	}
 	kh_destroy(glyphcache, ctx->glyph_cache);
 	ltk_free(ctx);
+	/* FIXME: figure out where this should go */
+	FcFini();
 }
 
 static ltk_glyph_info *
@@ -592,7 +594,7 @@ ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, i
 	if (w <= 0 || h <= 0)
 		return;
 	Drawable d = ltk_surface_get_drawable(s);
-	XImage *img = XGetImage(tl->ctx->window->renderdata->dpy, d, x, y, w, h, 0xFFFFFF, ZPixmap);
+	XImage *img = XGetImage(tl->ctx->data->dpy, d, x, y, w, h, 0xFFFFFF, ZPixmap);
 
 	int last_break = 0;
 	for (int i = 0; i < tl->lines; i++) {
@@ -608,7 +610,7 @@ ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, i
 		}
 		last_break = next_break;
 	}
-	XPutImage(tl->ctx->window->renderdata->dpy, d, tl->ctx->window->renderdata->gc, img, 0, 0, x, y, w, h);
+	XPutImage(tl->ctx->data->dpy, d, tl->ctx->data->gc, img, 0, 0, x, y, w, h);
 	XDestroyImage(img);
 }
 
diff --git a/src/util.c b/src/util.c
@@ -154,7 +154,7 @@ ltk_fatal_errno(const char *format, ...) {
 }
 
 int
-str_array_equal(char *terminated, char *array, size_t len) {
+str_array_equal(const char *terminated, const char *array, size_t len) {
 	if (!strncmp(terminated, array, len)) {
 		/* this is kind of inefficient, but there's no way to know
 		   otherwise if strncmp just stopped comparing after a '\0' */
diff --git a/src/util.h b/src/util.h
@@ -46,7 +46,7 @@ void ltk_warn(const char *format, ...);
  * Returns non-zero if they are equal, 0 otherwise.
  */
 /* Note: this doesn't work if array contains '\0'. */
-int str_array_equal(char *terminated, char *array, size_t len);
+int str_array_equal(const char *terminated, const char *array, size_t len);
 
 #define LENGTH(X) (sizeof(X) / sizeof(X[0]))
 
diff --git a/src/widget.c b/src/widget.c
@@ -26,14 +26,104 @@
 #include "util.h"
 #include "khash.h"
 #include "surface_cache.h"
-#include "widget_config.h"
 #include "config.h"
-
-struct ltk_key_callback {
+#include "array.h"
+#include "keys.h"
+
+static int cb_focus_active(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_unfocus_active(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_prev(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_next(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_left(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_right(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_up(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_move_down(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_set_pressed(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_unset_pressed(ltk_window *window, ltk_key_event *event, int handled);
+static int cb_remove_popups(ltk_window *window, ltk_key_event *event, int handled);
+
+struct key_cb {
 	char *func_name;
 	int (*callback)(ltk_window *, ltk_key_event *, int handled);
 };
 
+static struct key_cb cb_map[] = {
+	{"focus-active", &cb_focus_active},
+	{"move-down", &cb_move_down},
+	{"move-left", &cb_move_left},
+	{"move-next", &cb_move_next},
+	{"move-prev", &cb_move_prev},
+	{"move-right", &cb_move_right},
+	{"move-up", &cb_move_up},
+	{"remove-popups", &cb_remove_popups},
+	{"set-pressed", &cb_set_pressed},
+	{"unfocus-active", &cb_unfocus_active},
+	{"unset-pressed", &cb_unset_pressed},
+};
+
+
+struct keypress_cfg {
+	ltk_keypress_binding b;
+	struct key_cb cb;
+};
+
+struct keyrelease_cfg {
+	ltk_keyrelease_binding b;
+	struct key_cb cb;
+};
+
+LTK_ARRAY_INIT_DECL_STATIC(keypress, struct keypress_cfg)
+LTK_ARRAY_INIT_IMPL_STATIC(keypress, struct keypress_cfg)
+LTK_ARRAY_INIT_DECL_STATIC(keyrelease, struct keyrelease_cfg)
+LTK_ARRAY_INIT_IMPL_STATIC(keyrelease, struct keyrelease_cfg)
+
+static ltk_array(keypress) *keypresses = NULL;
+static ltk_array(keyrelease) *keyreleases = NULL;
+
+GEN_CB_MAP_HELPERS(cb_map, struct key_cb, func_name)
+
+/* FIXME: most of this is duplicated code */
+/* FIXME: document that pointers inside binding are taken over! */
+int ltk_widget_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b);
+int ltk_widget_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b);
+
+int
+ltk_widget_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b) {
+	if (!keypresses)
+		keypresses = ltk_array_create(keypress, 1);
+	struct key_cb *cb = cb_map_get_entry(func_name, func_len);
+	if (!cb)
+		return 1;
+	struct keypress_cfg cfg = {b, *cb};
+	ltk_array_append(keypress, keypresses, cfg);
+	return 0;
+}
+
+int
+ltk_widget_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b) {
+	if (!keyreleases)
+		keyreleases = ltk_array_create(keyrelease, 1);
+	struct key_cb *cb = cb_map_get_entry(func_name, func_len);
+	if (!cb)
+		return 1;
+	struct keyrelease_cfg cfg = {b, *cb};
+	ltk_array_append(keyrelease, keyreleases, cfg);
+	return 0;
+}
+
+static void
+destroy_keypress_cfg(struct keypress_cfg cfg) {
+	ltk_keypress_binding_destroy(cfg.b);
+}
+
+void
+ltk_widget_cleanup(void) {
+	ltk_array_destroy_deep(keypress, keypresses, &destroy_keypress_cfg);
+	ltk_array_destroy(keyrelease, keyreleases);
+	keypresses = NULL;
+	keyreleases = NULL;
+}
+
 static void ltk_destroy_widget_hash(void);
 
 KHASH_MAP_INIT_STR(widget, ltk_widget *)
@@ -826,30 +916,6 @@ cb_remove_popups(ltk_window *window, ltk_key_event *event, int handled) {
 	return 0;
 }
 
-static ltk_key_callback key_callbacks[] = {
-	{"move-next", &cb_move_next},
-	{"move-prev", &cb_move_prev},
-	{"move-up", &cb_move_up},
-	{"move-down", &cb_move_down},
-	{"move-left", &cb_move_left},
-	{"move-right", &cb_move_right},
-	{"focus-active", &cb_focus_active},
-	{"unfocus-active", &cb_unfocus_active},
-	{"set-pressed", &cb_set_pressed},
-	{"unset-pressed", &cb_unset_pressed},
-	{"remove-popups", &cb_remove_popups},
-};
-
-/* FIXME: binary search (copy from ledit) */
-ltk_key_callback *
-ltk_get_key_func(char *name, size_t len) {
-	for (size_t i = 0; i < LENGTH(key_callbacks); i++) {
-		if (str_array_equal(key_callbacks[i].func_name, name, len))
-			return &key_callbacks[i];
-	}
-	return NULL;
-}
-
 /* FIXME: should keyrelease events be ignored if the corresponding keypress event
    was consumed for movement? */
 /* FIXME: check if there's any weirdness when combining return and mouse press */
@@ -857,8 +923,6 @@ ltk_get_key_func(char *name, size_t len) {
 /* FIXME: implement key binding flag to run before widget handler is called */
 void
 ltk_window_key_press_event(ltk_window *window, ltk_key_event *event) {
-	/* FIXME: how to handle config being NULL? */
-	ltk_config *config = ltk_config_get();
 	int handled = 0;
 	if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
 		gen_widget_stack(window->active_widget);
@@ -870,20 +934,22 @@ ltk_window_key_press_event(ltk_window *window, ltk_key_event *event) {
 			}
 		}
 	}
+	if (!keypresses)
+		return;
 	ltk_keypress_binding *b = NULL;
-	for (size_t i = 0; i < config->keys.press_len; i++) {
-		b = &config->keys.press_bindings[i];
+	for (size_t i = 0; i < ltk_array_length(keypresses); i++) {
+		b = <k_array_get(keypresses, i).b;
 		if (b->mods != event->modmask || (!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) {
 			continue;
 		} else if (b->text) {
 			if (event->mapped && !strcmp(b->text, event->mapped))
-				handled |= b->callback->callback(window, event, handled);
+				handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
 		} else if (b->rawtext) {
 			if (event->text && !strcmp(b->text, event->text))
-				handled |= b->callback->callback(window, event, handled);
+				handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
 		} else if (b->sym != LTK_KEY_NONE) {
 			if (event->sym == b->sym)
-				handled |= b->callback->callback(window, event, handled);
+				handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
 		}
 	}
 
@@ -893,7 +959,6 @@ ltk_window_key_press_event(ltk_window *window, ltk_key_event *event) {
 void
 ltk_window_key_release_event(ltk_window *window, ltk_key_event *event) {
 	/* FIXME: emit event */
-	ltk_config *config = ltk_config_get();
 	int handled = 0;
 	if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
 		gen_widget_stack(window->active_widget);
@@ -904,13 +969,15 @@ ltk_window_key_release_event(ltk_window *window, ltk_key_event *event) {
 			}
 		}
 	}
+	if (!keyreleases)
+		return;
 	ltk_keyrelease_binding *b = NULL;
-	for (size_t i = 0; i < config->keys.release_len; i++) {
-		b = &config->keys.release_bindings[i];
+	for (size_t i = 0; i < ltk_array_length(keyreleases); i++) {
+		b = <k_array_get(keyreleases, i).b;
 		if (b->mods != event->modmask || (!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) {
 			continue;
 		} else if (b->sym != LTK_KEY_NONE && event->sym == b->sym) {
-			handled |= b->callback->callback(window, event, handled);
+			handled |= ltk_array_get(keyreleases, i).cb.callback(window, event, handled);
 		}
 	}
 }
diff --git a/src/widget.h b/src/widget.h
@@ -20,10 +20,10 @@
 #include "err.h"
 #include "rect.h"
 #include "event.h"
+#include "config.h"
 
 /* FIXME: SORT OUT INCLUDES PROPERLY! */
 
-
 typedef struct ltk_widget ltk_widget;
 typedef uint32_t ltk_widget_type;
 
@@ -31,6 +31,9 @@ typedef enum {
 	LTK_ACTIVATABLE_NORMAL = 1,
 	LTK_ACTIVATABLE_SPECIAL = 2,
 	LTK_ACTIVATABLE_ALWAYS = 1|2,
+	/* FIXME: redundant or needs better name - is implied by entries in vtable
+	   - if there are widgets that have keyboard functions in the vtable but
+	     shouldn't have this set, then it's a bad name */
 	LTK_NEEDS_KEYBOARD = 4,
 	LTK_NEEDS_REDRAW = 8,
 	LTK_HOVER_IS_ACTIVE = 16,
@@ -113,8 +116,6 @@ struct ltk_widget {
 	char hidden;
 };
 
-/* FIXME: just give the structs for the actual event type here instead
-   of the generic ltk_event */
 struct ltk_widget_vtable {
 	int (*key_press)(struct ltk_widget *, ltk_key_event *);
 	int (*key_release)(struct ltk_widget *, ltk_key_event *);
@@ -174,7 +175,6 @@ int ltk_widget_id_free(const char *id);
 ltk_widget *ltk_get_widget(const char *id, ltk_widget_type type, ltk_error *err);
 void ltk_set_widget(ltk_widget *widget, const char *id);
 void ltk_remove_widget(const char *id);
-void ltk_widgets_cleanup();
 void ltk_widgets_init();
 void ltk_widget_resize(ltk_widget *widget);
 void ltk_widget_remove_client(int client);
@@ -191,4 +191,10 @@ void ltk_widget_remove_from_event_lmask(ltk_widget *widget, int client, uint32_t
 void ltk_widget_remove_from_event_wmask(ltk_widget *widget, int client, uint32_t mask);
 void ltk_widget_remove_from_event_lwmask(ltk_widget *widget, int client, uint32_t mask);
 
+
+/* FIXME: document that pointers inside binding are taken over! */
+int ltk_widget_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b);
+int ltk_widget_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b);
+void ltk_widget_cleanup(void);
+
 #endif /* LTK_WIDGET_H */
diff --git a/src/widget_config.h b/src/widget_config.h
@@ -1,7 +0,0 @@
-#ifndef LTK_WIDGET_CONFIG_H
-#define LTK_WIDGET_CONFIG_H
-
-typedef struct ltk_key_callback ltk_key_callback;
-ltk_key_callback *ltk_get_key_func(char *name, size_t len);
-
-#endif /* LTK_WIDGET_CONFIG_H */
diff --git a/test3.gui b/test3.gui
@@ -1,7 +1,8 @@
-grid grd1 create 3 1
+grid grd1 create 4 1
 grid grd1 set-row-weight 0 1
 grid grd1 set-row-weight 1 1
 grid grd1 set-row-weight 2 1
+grid grd1 set-row-weight 3 1
 grid grd1 set-column-weight 0 1
 set-root-widget grd1
 button btn1 create "I'm a button!"
@@ -11,3 +12,5 @@ grid grd1 add btn1 0 0 1 1
 grid grd1 add btn2 1 0 1 1
 grid grd1 add btn3 2 0 1 1
 mask-add btn1 button press
+entry entry1 create "Hi"
+grid grd1 add entry1 3 0 1 1 ew
diff --git a/test3.sh b/test3.sh
@@ -2,10 +2,10 @@
 
 export LTKDIR="`pwd`/.ltk"
 ltk_id=`./src/ltkd -t "Cool Window"`
-if [ $? -ne 0 ]; then
-	echo "Unable to start ltkd." >&2
-	exit 1
-fi
+#if [ $? -ne 0 ]; then
+#	echo "Unable to start ltkd." >&2
+#	exit 1
+#fi
 
 cat test3.gui | ./src/ltkc $ltk_id | while read cmd
 do