commit d02312cfb88d3c52be0c9c4e421af28b224fd7b5
parent 55009a9b2fc3477235ddf5cae6c45446e0117802
Author: lumidify <nobody@lumidify.org>
Date:   Thu, 24 Dec 2020 14:06:27 +0100
Use sockets instead of stdin and stdout
Diffstat:
| M | .gitignore |  |  | 4 | +++- | 
| M | Makefile |  |  | 11 | ++++++++--- | 
| M | README.md |  |  | 7 | ++++--- | 
| A | TODO |  |  | 15 | +++++++++++++++ | 
| M | button.c |  |  | 2 | +- | 
| D | ltk.c |  |  | 646 | ------------------------------------------------------------------------------- | 
| M | ltk.h |  |  | 30 | ++++++++++++++++++------------ | 
| A | ltkc.c |  |  | 154 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | ltkd.c |  |  | 912 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| M | socket_format.txt |  |  | 4 | ++-- | 
| A | test.sh |  |  | 27 | +++++++++++++++++++++++++++ | 
| D | test_anim.sh |  |  | 29 | ----------------------------- | 
| D | test_draw.gui |  |  | 10 | ---------- | 
13 files changed, 1144 insertions(+), 707 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,3 +1,5 @@
-ltk
+ltkd
+ltkc
+ltk.sock
 *.o
 *.core
diff --git a/Makefile b/Makefile
@@ -1,14 +1,19 @@
 include config.mk
 
-OBJ += color.o util.o ltk.o ini.o grid.o button.o draw.o
+OBJ += color.o util.o ltkd.o ini.o grid.o button.o draw.o
 
-ltk: $(OBJ) $(COMPATOBJ)
+all: ltkd ltkc
+
+ltkd: $(OBJ) $(COMPATOBJ)
 	$(CC) -o $@ $(OBJ) $(COMPATOBJ) $(LDFLAGS)
 
+ltkc: ltkc.o
+	$(CC) -o $@ ltkc.o
+
 %.o: %.c
 	$(CC) -c -o $@ $< $(CFLAGS)
 
 .PHONY: clean
 
 clean:
-	rm -f $(OBJ) ltk *.core
+	rm -f $(OBJ) ltkc.o ltkd ltkc ltk.sock *.core
diff --git a/README.md b/README.md
@@ -11,9 +11,10 @@ because it's a bit of a hack.
 To test:
 
 make
-./ltk < test.gui
-./ltk < test_draw.gui
-./test_anim.sh | ./ltk (this requires sleep(1) to support fractional seconds)
+./test.sh
+
+If you click the top button, it should exit. That's all it does now.
+Also read the comment in './test.sh'.
 
 Note: you need to uncomment "COMPATOBJ = strtonum.c" in config.mk
 if you're not using OpenBSD.
diff --git a/TODO b/TODO
@@ -0,0 +1,15 @@
+Convert points to pixels for stb rendering (currently, the size between
+pango and stb is completely different).
+
+Random stuff:
+* This is not really a general-purpose GUI toolkit - imagine building
+  a complex GUI with this. Especially things like having to respond
+  directly to mouse movement wouldn't be very efficient due to all the
+  protocol overhead (or would it maybe not be as bad as I think?).
+  One idea would be to have a C API and the socket API. The C program
+  works more or less like a regular GUI program but the GUI mainloop
+  still opens the socket as usual and allows commands to be sent that
+  way. That way, you would get the advantages of both sides - the C
+  program can do the normal GUI stuff, but everything can still be
+  edited on-the-fly, and text can be retrieved (e.g. for a screen-
+  reader).
diff --git a/button.c b/button.c
@@ -154,7 +154,7 @@ ltk_button_change_state(ltk_button *button) {
 
 static void
 ltk_button_mouse_release(ltk_button *button, XEvent event) {
-	ltk_queue_event(button->widget.window, button->widget.id, "button_click");
+	ltk_queue_event(button->widget.window, LTK_EVENT_BUTTON, button->widget.id, "button_click");
 }
 
 static ltk_button *
diff --git a/ltk.c b/ltk.c
@@ -1,646 +0,0 @@
-/*
- * This file is part of the Lumidify ToolKit (LTK)
- * Copyright (c) 2016, 2017, 2018, 2020 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.
- */
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <stdint.h>
-#include <fcntl.h>
-#include <unistd.h>
-#include <time.h>
-#include <sys/stat.h>
-#include <sys/select.h>
-#include <X11/Xlib.h>
-#include <X11/Xutil.h>
-#include "util.h"
-#include "khash.h"
-#include "ini.h"
-#include "text.h"
-#include "ltk.h"
-#include "grid.h"
-#include "button.h"
-#include "draw.h"
-
-static void ltk_load_theme(ltk_window *window, const char *path);
-static void ltk_destroy_theme(ltk_theme *theme);
-static ltk_rect ltk_rect_union(ltk_rect r1, ltk_rect r2);
-
-static int running = 1;
-static char *cmd_input = NULL;
-static char **tokens = NULL;
-static size_t cmd_bufsize = 0;
-static size_t tokens_bufsize = 0;
-static size_t cmd_len = 0;
-static size_t tokens_len = 0;
-
-static ltk_rect
-ltk_rect_union(ltk_rect r1, ltk_rect r2) {
-	ltk_rect u;
-	u.x = r1.x < r2.x ? r1.x : r2.x;
-	u.y = r1.y < r2.y ? r1.y : r2.y;
-	int x2 = r1.x + r1.w < r2.x + r2.w ? r2.x + r2.w : r1.x + r1.w;
-	int y2 = r1.y + r1.h < r2.y + r2.h ? r2.y + r2.h : r1.y + r1.h;
-	u.w = x2 - u.x;
-	u.h = y2 - u.y;
-	return u;
-}
-
-void
-ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect) {
-	if (window->dirty_rect.w == 0 && window->dirty_rect.h == 0)
-		window->dirty_rect = rect;
-	else
-		window->dirty_rect = ltk_rect_union(rect, window->dirty_rect);
-}
-
-void
-ltk_clean_up(ltk_window *window) {
-	ltk_destroy_theme(window->theme);
-	ltk_destroy_window(window);
-	if (tokens) free(tokens);
-	if (cmd_input) free(cmd_input);
-}
-
-void
-ltk_quit(ltk_window *window) {
-	ltk_clean_up(window);
-	running = 0;
-}
-
-void
-ltk_fatal(const char *msg) {
-	(void)fprintf(stderr, msg);
-	/* FIXME: clean up first */
-	exit(1);
-};
-
-int
-ltk_create_xcolor(ltk_window *window, const char *hex, XColor *col) {
-	if (!XParseColor(window->dpy, window->cm, hex, col)) {
-		(void)fprintf(stderr, "Invalid color: %s\n", hex);
-		return 0;
-	}
-	XAllocColor(window->dpy, window->cm, col);
-
-	return 1;
-}
-
-void
-ltk_queue_event(ltk_window *window, const char *id, const char *name) {
-	struct ltk_event_queue *new = malloc(sizeof(struct ltk_event_queue));
-	if (!new) ltk_fatal("Unable to queue event.\n");
-	new->id = strdup(id);
-	new->name = strdup(name);
-	new->next = window->first_event;
-	window->first_event = new;
-	new->prev = NULL;
-	if (!window->last_event)
-		window->last_event = new;
-}
-
-static void
-ltk_set_root_widget_cmd(
-    ltk_window *window,
-    char **tokens,
-    int num_tokens) {
-	ltk_widget *widget;
-	if (num_tokens != 2) {
-		(void)fprintf(stderr, "set-root-widget: Invalid number of arguments.\n");
-		return;
-	}
-	widget = ltk_get_widget(window, tokens[1], LTK_WIDGET, "set-root-widget");
-	if (!widget) return;
-	window->root_widget = widget;
-	int w = widget->rect.w;
-	int h = widget->rect.h;
-	widget->rect.w = window->rect.w;
-	widget->rect.h = window->rect.h;
-	if (widget->resize) {
-		widget->resize(widget, w, h);
-	}
-}
-
-/* copied from suckless ii */
-static int
-read_cmdline(int fd) {
-	char c = '\0';
-	char *tmp = NULL;
-	cmd_len = 0;
-	do {
-		if (cmd_len >= cmd_bufsize) {
-			cmd_bufsize = !cmd_bufsize ? 1 : cmd_bufsize;
-			tmp = realloc(cmd_input, cmd_bufsize * 2 * sizeof(char));
-			if (!tmp) ltk_fatal("Out of memory while reading command.\n");
-			cmd_input = tmp;
-			cmd_bufsize *= 2;
-		}
-		if (read(fd, &c, sizeof(char)) != sizeof(char))
-			return -1;
-		cmd_input[cmd_len++] = c;
-	} while (c != '\n');
-	cmd_input[cmd_len - 1] = '\0'; /* eliminates '\n' */
-	return 0;
-}
-
-/* FIXME: turn this into something that doesn't, you know,
-   explode at the slightest touch... */
-static int
-tokenize(void) {
-	char *c;
-	tokens_len = 0;
-	if (!cmd_input || cmd_input[0] == '\0') return 0;
-	for (c = cmd_input; *c == ' '; c++)
-		;
-	size_t cur_tok = 0;
-	int in_str = 0;
-	char **tmp;
-	if (tokens_bufsize)
-		tokens[0] = c;
-	while (*c != '\0') {
-		if (cur_tok >= tokens_bufsize) {
-			if (!tokens_bufsize)
-				tokens_bufsize = 1;
-			tmp = realloc(tokens, tokens_bufsize * 2 * sizeof(char *));
-			if (!tmp) ltk_fatal("Out of memory while parsing command.\n");
-			tokens = tmp;
-			tokens[cur_tok] = c;
-			tokens_bufsize *= 2;
-		}
-		if (*c == '"') {
-			if (!in_str)
-				tokens[cur_tok] = c + 1;
-			in_str = !in_str;
-			*c = '\0';
-		} else if (*c == ' ' && !in_str) {
-			*c = '\0';
-			cur_tok++;
-			tokens[cur_tok] = c + 1;
-		}
-		c++;
-	}
-	tokens_len = cur_tok + 1;
-}
-
-static void
-proc_cmds(ltk_window *window) {
-	if (!tokenize()) return;
-	if (!tokens_len) return;
-	if (strcmp(tokens[0], "grid") == 0) {
-		ltk_grid_cmd(window, tokens, tokens_len);
-	} else if (strcmp(tokens[0], "button") == 0) {
-		ltk_button_cmd(window, tokens, tokens_len);
-	} else if (strcmp(tokens[0], "set-root-widget") == 0) {
-		ltk_set_root_widget_cmd(window, tokens, tokens_len);
-	} else if (strcmp(tokens[0], "draw") == 0) {
-		ltk_draw_cmd(window, tokens, tokens_len);
-	} else if (strcmp(tokens[0], "quit") == 0) {
-		ltk_quit(window);
-	} else {
-		(void)fprintf(stderr, "Invalid command.\n");
-	}
-}
-
-int
-ltk_mainloop(ltk_window *window) {
-	XEvent event;
-        struct stat st;
-        struct timespec tick;
-	struct timeval tv;
-	fd_set rfds;
-	int fd_in = fileno(stdin);
-	int retval;
-	tick.tv_sec = 0;
-	tick.tv_nsec = 10000;
-	tv.tv_sec = 0;
-	tv.tv_usec = 0;
-
-	tokens = malloc(10 * sizeof(char *));
-	cmd_input = malloc(200 * sizeof(char));
-	if (!tokens || !cmd_input) ltk_fatal("Out of memory.\n");
-	tokens_bufsize = 10;
-	cmd_bufsize = 200;
-
-	while (running) {
-		FD_ZERO(&rfds);
-		FD_SET(fd_in, &rfds);
-		retval = select(fd_in + 1, &rfds, NULL, NULL, &tv);
-		while (XPending(window->dpy)) {
-			XNextEvent(window->dpy, &event);
-			ltk_handle_event(window, event);
-		}
-		if (retval > 0 && !read_cmdline(fd_in)) {
-			proc_cmds(window);
-		} else if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) {
-			ltk_redraw_window(window);
-			window->dirty_rect.w = 0;
-			window->dirty_rect.h = 0;
-		}
-		if (window->last_event && running) {
-			struct ltk_event_queue *cur = window->last_event;
-			struct ltk_event_queue *last;
-			do {
-				printf("%s %s\n", cur->id, cur->name);
-				free(cur->id);
-				free(cur->name);
-				last = cur;
-				cur = cur->prev;
-				free(last);
-			} while (cur);
-			window->first_event = window->last_event = NULL;
-		} 
-		/* yes, this should be improved */
-		nanosleep(&tick, NULL);
-
-	}
-}
-
-void
-ltk_redraw_window(ltk_window *window) {
-	ltk_widget *ptr;
-	if (!window) return;
-	if (window->dirty_rect.x >= window->rect.w) return;
-	if (window->dirty_rect.y >= window->rect.h) return;
-	if (window->dirty_rect.x + window->dirty_rect.w > window->rect.w)
-		window->dirty_rect.w -= window->dirty_rect.x + window->dirty_rect.w - window->rect.w;
-	if (window->dirty_rect.y + window->dirty_rect.h > window->rect.h)
-		window->dirty_rect.h -= window->dirty_rect.y + window->dirty_rect.h - window->rect.h;
-	XClearArea(window->dpy, window->xwindow, window->dirty_rect.x, window->dirty_rect.y, window->dirty_rect.w, window->dirty_rect.h, False);
-	if (!window->root_widget) return;
-	/* FIXME: actually respect the dirty rect... */
-	ptr = window->root_widget;
-	ptr->draw(ptr);
-}
-
-ltk_window *
-ltk_create_window(const char *theme_path, const char *title, int x, int y, unsigned int w, unsigned int h) {
-	ltk_window *window = malloc(sizeof(ltk_window));
-	if (!window)
-		ltk_fatal("Not enough memory left for window!\n");
-
-	window->dpy = XOpenDisplay(NULL);
-	window->screen = DefaultScreen(window->dpy);
-	window->cm = DefaultColormap(window->dpy, window->screen);
-	/* FIXME: validate theme properly */
-	ltk_load_theme(window, theme_path);
-	window->wm_delete_msg = XInternAtom(window->dpy, "WM_DELETE_WINDOW", False);
-
-	ltk_window_theme *wtheme = window->theme->window;
-	window->xwindow =
-	    XCreateSimpleWindow(window->dpy, DefaultRootWindow(window->dpy), x, y,
-				w, h, wtheme->border_width,
-				wtheme->fg.pixel, wtheme->bg.pixel);
-	window->gc = XCreateGC(window->dpy, window->xwindow, 0, 0);
-	XSetForeground(window->dpy, window->gc, wtheme->fg.pixel);
-	XSetBackground(window->dpy, window->gc, wtheme->bg.pixel);
-	XSetStandardProperties(window->dpy, window->xwindow, title, NULL, None,
-			       NULL, 0, NULL);
-	XSetWMProtocols(window->dpy, window->xwindow, &window->wm_delete_msg, 1);
-	window->root_widget = NULL;
-
-	ltk_init_text(wtheme->font, window->dpy, window->screen, window->cm);
-
-	window->other_event = <k_window_other_event;
-
-	window->rect.w = 0;
-	window->rect.h = 0;
-	window->rect.x = 0;
-	window->rect.y = 0;
-	window->dirty_rect.w = 0;
-	window->dirty_rect.h = 0;
-	window->dirty_rect.x = 0;
-	window->dirty_rect.y = 0;
-
-	window->widget_hash = kh_init(widget);
-
-	XClearWindow(window->dpy, window->xwindow);
-	XMapRaised(window->dpy, window->xwindow);
-	XSelectInput(window->dpy, window->xwindow,
-		     ExposureMask | KeyPressMask | KeyReleaseMask |
-		     ButtonPressMask | ButtonReleaseMask |
-		     StructureNotifyMask | PointerMotionMask);
-
-	return window;
-}
-
-void
-ltk_destroy_window(ltk_window *window) {
-	khint_t k;
-	ltk_widget *ptr;
-	XDestroyWindow(window->dpy, window->xwindow);
-	for (k = kh_begin(window->widget_hash); k != kh_end(window->widget_hash); k++) {
-		if (kh_exist(window->widget_hash, k)) {
-			ptr = kh_value(window->widget_hash, k);
-			ptr->destroy(ptr, 1);
-		}
-	}
-	kh_destroy(widget, window->widget_hash);
-	ltk_cleanup_text();
-	XCloseDisplay(window->dpy);
-	free(window);
-}
-
-void
-ltk_window_other_event(ltk_window *window, XEvent event) {
-	ltk_widget *ptr = window->root_widget;
-	int retval = 0;
-	if (event.type == ConfigureNotify) {
-		unsigned int w, h;
-		w = event.xconfigure.width;
-		h = event.xconfigure.height;
-		int orig_w = window->rect.w;
-		int orig_h = window->rect.h;
-		if (orig_w != w || orig_h != h) {
-			window->rect.w = w;
-			window->rect.h = h;
-			if (ptr && ptr->resize) {
-				ptr->rect.w = w;
-				ptr->rect.h = h;
-				ptr->resize(ptr, orig_w, orig_h);
-			}
-		}
-	} else if (event.type == Expose && event.xexpose.count == 0) {
-		ltk_rect r;
-		r.x = event.xexpose.x;
-		r.y = event.xexpose.y;
-		r.w = event.xexpose.width;
-		r.h = event.xexpose.height;
-		ltk_window_invalidate_rect(window, r);
-	} else if (event.type == ClientMessage
-	    && event.xclient.data.l[0] == window->wm_delete_msg) {
-		ltk_destroy_window(window);
-		exit(0); /* FIXME */
-	}
-}
-
-void
-ltk_window_ini_handler(ltk_window *window, const char *prop, const char *value) {
-	/* FIXME: A whole lot more error checking! */
-	if (strcmp(prop, "border_width") == 0) {
-		window->theme->window->border_width = atoi(value);
-	} else if (strcmp(prop, "bg") == 0) {
-		ltk_create_xcolor(window, value, &window->theme->window->bg);
-	} else if (strcmp(prop, "fg") == 0) {
-		ltk_create_xcolor(window, value, &window->theme->window->fg);
-	} else if (strcmp(prop, "font") == 0) {
-		window->theme->window->font = strdup(value);
-	} else if (strcmp(prop, "font_size") == 0) {
-		window->theme->window->font_size = atoi(value);
-	}
-}
-
-int
-ltk_ini_handler(ltk_window *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);
-	}
-}
-
-static void
-ltk_load_theme(ltk_window *window, const char *path) {
-	window->theme = malloc(sizeof(ltk_theme));
-	if (!window->theme) ltk_fatal("Unable to allocate memory for theme.\n");
-	window->theme->window = malloc(sizeof(ltk_window_theme));
-	if (!window->theme->window) ltk_fatal("Unable to allocate memory for window theme.\n");
-	window->theme->button = NULL;
-	if (ini_parse(path, ltk_ini_handler, window) < 0) {
-		(void)fprintf(stderr, "ERROR: Can't load theme %s\n.", path);
-		exit(1);
-	}
-}
-
-static void
-ltk_destroy_theme(ltk_theme *theme) {
-	free(theme->button);
-	free(theme->window);
-	free(theme);
-}
-
-int
-ltk_collide_rect(ltk_rect rect, int x, int y) {
-	return (rect.x <= x && (rect.x + rect.w) >= x && rect.y <= y
-		&& (rect.y + rect.h) >= y);
-}
-
-void
-ltk_window_remove_active_widget(ltk_window *window) {
-	ltk_widget *widget = window->active_widget;
-	if (!widget) return;
-	while (widget) {
-		widget->state = LTK_NORMAL;
-		widget->active_widget = NULL;
-		if (widget->change_state)
-			widget->change_state(widget);
-		if (widget->needs_redraw)
-			ltk_window_invalidate_rect(window, widget->rect);
-		widget = widget->parent;
-	}
-	window->active_widget = NULL;
-}
-
-void
-ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
-	window->active_widget = widget;
-	ltk_widget *parent = widget->parent;
-	widget->state = LTK_ACTIVE;
-	while (parent) {
-		widget->state = LTK_ACTIVE;
-		parent->active_widget = widget;
-		widget = parent;
-		parent = widget->parent;
-	}
-}
-
-void
-ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window *window,
-    void (*draw) (void *), void (*change_state) (void *),
-    void (*destroy) (void *, int), unsigned int needs_redraw,
-    ltk_widget_type type) {
-	widget->id = strdup(id);
-	widget->window = window;
-	widget->active_widget = NULL;
-	widget->parent = NULL;
-	widget->type = type;
-
-	widget->key_press = NULL;
-	widget->key_release = NULL;
-	widget->mouse_press = NULL;
-	widget->mouse_release = NULL;
-	widget->motion_notify = NULL;
-	widget->mouse_enter = NULL;
-	widget->mouse_leave = NULL;
-
-	widget->resize = NULL;
-	widget->draw = draw;
-	widget->change_state = change_state;
-	widget->destroy = destroy;
-
-	widget->needs_redraw = needs_redraw;
-	widget->state = LTK_NORMAL;
-	widget->row = 0;
-	widget->rect.x = 0;
-	widget->rect.y = 0;
-	widget->rect.w = 100;
-	widget->rect.h = 100;
-
-	widget->row = NULL;
-	widget->column = NULL;
-	widget->row_span = NULL;
-	widget->column_span = NULL;
-	widget->sticky = 0;
-}
-
-void
-ltk_widget_mouse_press_event(ltk_widget *widget, XEvent event) {
-	if (!widget || widget->state == LTK_DISABLED)
-		return;
-	if (event.xbutton.button == 1) {
-		ltk_widget *parent = widget->parent;
-		widget->state = LTK_PRESSED;
-		if (widget->change_state)
-			widget->change_state(widget);
-		if (widget->needs_redraw)
-			ltk_window_invalidate_rect(widget->window, widget->rect);
-	}
-	if (widget->mouse_press) {
-		widget->mouse_press(widget, event);
-	}
-}
-
-void
-ltk_widget_mouse_release_event(ltk_widget *widget, XEvent event) {
-	if (!widget || widget->state == LTK_DISABLED)
-		return;
-	if (widget->state == LTK_PRESSED) {
-		widget->state = LTK_ACTIVE;
-		if (widget->change_state)
-			widget->change_state(widget);
-		if (widget->needs_redraw)
-			ltk_window_invalidate_rect(widget->window, widget->rect);
-	}
-	if (widget->mouse_release) {
-		widget->mouse_release(widget, event);
-	}
-}
-
-void
-ltk_widget_motion_notify_event(ltk_widget *widget, XEvent event) {
-	if (!widget) return;
-	short pressed = (event.xmotion.state & Button1Mask) == Button1Mask;
-	if ((widget->state == LTK_NORMAL) && !pressed) {
-		widget->state = LTK_ACTIVE;
-		if (widget->change_state)
-			widget->change_state(widget);
-		if (widget->mouse_enter)
-			widget->mouse_enter(widget, event);
-		/* FIXME: do this properly */
-		if (widget->window->active_widget != widget && !widget->motion_notify) {
-			ltk_window_remove_active_widget(widget->window);
-			ltk_window_set_active_widget(widget->window, widget);
-		}
-		if (widget->needs_redraw)
-			ltk_window_invalidate_rect(widget->window, widget->rect);
-	}
-	if (widget->motion_notify)
-		widget->motion_notify(widget, event);
-}
-
-void
-ltk_handle_event(ltk_window *window, XEvent event) {
-	ltk_widget *root_widget = window->root_widget;
-	switch (event.type) {
-	case KeyPress:
-		break;
-	case KeyRelease:
-		break;
-	case ButtonPress:
-		if (root_widget)
-			ltk_widget_mouse_press_event(root_widget, event);
-		break;
-	case ButtonRelease:
-		if (root_widget)
-			ltk_widget_mouse_release_event(root_widget, event);
-		break;
-	case MotionNotify:
-		if (root_widget)
-			ltk_widget_motion_notify_event(root_widget, event);
-		break;
-	default:
-		if (window->other_event)
-			window->other_event(window, event);
-	}
-}
-
-int
-ltk_check_widget_id_free(ltk_window *window, const char *id, const char *caller) {
-	khint_t k;
-	k = kh_get(widget, window->widget_hash, id);
-	if (k != kh_end(window->widget_hash)) {
-		(void)fprintf(stderr, "%s: Widget \"%s\" already exists.\n", caller, id);
-		return 0;
-	}
-	return 1;
-}
-
-ltk_widget *
-ltk_get_widget(ltk_window *window, const char *id, ltk_widget_type type,
-    const char *caller) {
-	khint_t k;
-	ltk_widget *widget;
-	k = kh_get(widget, window->widget_hash, id);
-	if (k == kh_end(window->widget_hash)) {
-		(void)fprintf(stderr, "%s: Widget \"%s\" doesn't exist.\n", caller, id);
-		return NULL;
-	}
-	widget = kh_value(window->widget_hash, k);
-	if (type != LTK_WIDGET && widget->type != type) {
-		(void)fprintf(stderr, "%s: Widget \"%s\" has wrong type.\n", caller, id);
-		return NULL;
-	}
-	return widget;
-}
-
-void
-ltk_set_widget(ltk_window *window, ltk_widget *widget, const char *id) {
-	int ret;
-	khint_t k;
-	/* apparently, khash requires the string to stay accessible */
-	char *tmp = strdup(id);
-	k = kh_put(widget, window->widget_hash, tmp, &ret);
-	kh_value(window->widget_hash, k) = widget;
-}
-
-void
-ltk_remove_widget(ltk_window *window, const char *id) {
-	khint_t k;
-	k = kh_get(widget, window->widget_hash, id);
-	if (k != kh_end(window->widget_hash)) {
-		kh_del(widget, window->widget_hash, k);
-	}
-}
-
-int main(int argc, char *argv[]) {
-	ltk_window *window = ltk_create_window("theme.ini", "Demo", 0, 0, 500, 500);
-	ltk_mainloop(window);
-}
diff --git a/ltk.h b/ltk.h
@@ -28,6 +28,12 @@
 
 #include "khash.h"
 
+typedef enum {
+	LTK_EVENT_RESIZE = 1 << 0,
+	LTK_EVENT_BUTTON = 1 << 1,
+	LTK_EVENT_KEY = 1 << 2
+} ltk_event_type;
+
 typedef struct {
 	int x;
 	int y;
@@ -59,10 +65,10 @@ typedef enum {
 typedef struct ltk_window ltk_window;
 
 typedef struct ltk_widget {
+	ltk_rect rect;
 	ltk_window *window;
 	struct ltk_widget *active_widget;
 	struct ltk_widget *parent;
-	ltk_widget_type type;
 	char *id;
 
 	void (*key_press) (void *, XEvent event);
@@ -78,14 +84,14 @@ typedef struct ltk_widget {
 	void (*change_state) (void *);
 	void (*destroy) (void *, int);
 
-	ltk_rect rect;
-	unsigned int row;
-	unsigned int column;
-	unsigned int row_span;
-	unsigned int column_span;
-	unsigned int needs_redraw;
+	ltk_widget_type type;
 	ltk_widget_state state;
-	unsigned short sticky;
+	unsigned int sticky;
+	unsigned short row;
+	unsigned short column;
+	unsigned short row_span;
+	unsigned short column_span;
+	unsigned char needs_redraw;
 } ltk_widget;
 
 typedef struct {
@@ -104,8 +110,8 @@ typedef struct {
 } ltk_theme;
 
 struct ltk_event_queue {
-	char *id;
-	char *name;
+	ltk_event_type event_type;
+	char *data;
 	struct ltk_event_queue *prev;
 	struct ltk_event_queue *next;
 };
@@ -121,7 +127,7 @@ typedef struct ltk_window {
 	Window xwindow;
 	ltk_widget *root_widget;
 	ltk_widget *active_widget;
-	int (*other_event) (ltk_window *, XEvent event);
+	void (*other_event) (ltk_window *, XEvent event);
 	ltk_rect rect;
 	ltk_theme *theme;
 	ltk_rect dirty_rect;
@@ -136,7 +142,7 @@ void ltk_fatal(const char *msg);
 
 int ltk_create_xcolor(ltk_window *window, const char *hex, XColor *col);
 
-void ltk_queue_event(ltk_window *window, const char *id, const char *name);
+void ltk_queue_event(ltk_window *window, ltk_event_type type, const char *id, const char *data);
 
 int ltk_mainloop(ltk_window *window);
 
diff --git a/ltkc.c b/ltkc.c
@@ -0,0 +1,154 @@
+/*
+ * This file is part of the Lumidify ToolKit (LTK)
+ * Copyright (c) 2020 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <time.h>
+#include <errno.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#define BLK_SIZE 128
+
+/* If `needed` is larger than `*alloc_size`, resize `*str` to `*alloc_size * 2`. */
+static int
+grow_string(char **str, int *alloc_size, int needed) {
+	if (needed <= *alloc_size) return 0;
+	char *new = realloc(*str, *alloc_size * 2);
+	if (!new) return 1;
+	*str = new;
+	*alloc_size = *alloc_size * 2;
+	return 0;
+}
+static struct {
+	char *in_buffer;
+	int in_len;
+	int in_alloc;
+	char *out_buffer;
+	int out_len;
+	int out_alloc;
+} io_buffers;
+
+int main(int argc, char *argv[]) {
+	int sockfd, maxrfd, maxwfd;
+	int infd = fileno(stdin);
+	int outfd = fileno(stdout);
+	struct sockaddr_un un;
+	fd_set rfds, wfds, rallfds, wallfds;
+	struct timeval tv;
+	tv.tv_sec = 0;
+	tv.tv_usec = 15;
+
+	if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
+		perror("Socket error");
+		return -1;
+	}
+	memset(&un, 0, sizeof(un));
+	un.sun_family = AF_UNIX;
+	strcpy(un.sun_path, "ltk.sock");
+	if (connect(sockfd, (struct sockaddr *)&un, offsetof(struct sockaddr_un, sun_path) + 9) < 0) {
+		perror("Socket error");
+		return -2;
+	}
+
+	io_buffers.in_buffer = malloc(BLK_SIZE);
+	if (!io_buffers.in_buffer)
+		return 1;
+	io_buffers.in_alloc = BLK_SIZE;
+
+	io_buffers.out_buffer = malloc(BLK_SIZE);
+	if (!io_buffers.out_buffer)
+		return 1;
+	io_buffers.out_alloc = BLK_SIZE;
+
+	FD_ZERO(&rallfds);
+	FD_ZERO(&wallfds);
+
+	FD_SET(sockfd, &rallfds);
+	FD_SET(infd, &rallfds);
+	FD_SET(sockfd, &wallfds);
+	FD_SET(outfd, &wallfds);
+	maxrfd = sockfd > infd ? sockfd : infd;
+	maxwfd = sockfd > outfd ? sockfd : outfd;
+
+	while (1) {
+		if (!FD_ISSET(infd, &rallfds) && !FD_ISSET(sockfd, &rallfds) && io_buffers.in_len == 0 && io_buffers.out_len == 0)
+			break;
+		rfds = rallfds;
+		wfds = wallfds;
+		/* Separate this because the writing fds are *usually* always ready,
+		   leading to the loop looping way too fast */
+		select(maxrfd + 1, &rfds, NULL, NULL, &tv);
+		select(maxwfd + 1, NULL, &wfds, NULL, &tv);
+		if (FD_ISSET(sockfd, &rfds)) {
+			grow_string(&io_buffers.out_buffer, &io_buffers.out_alloc, io_buffers.out_len + BLK_SIZE);
+			int nread = read(sockfd, io_buffers.out_buffer + io_buffers.out_len, BLK_SIZE);
+			if (nread < 0) {
+				return 2;
+			} else if (nread == 0) {
+				FD_CLR(sockfd, &rallfds);
+				FD_CLR(sockfd, &wallfds);
+			} else {
+				io_buffers.out_len += nread;
+			}
+		}
+		if (FD_ISSET(infd, &rfds)) {
+			grow_string(&io_buffers.in_buffer, &io_buffers.in_alloc, io_buffers.in_len + BLK_SIZE);
+			int nread = read(infd, io_buffers.in_buffer + io_buffers.in_len, BLK_SIZE);
+			if (nread < 0) {
+				return 2;
+			} else if (nread == 0) {
+				FD_CLR(infd, &rallfds);
+			} else {
+				io_buffers.in_len += nread;
+			}
+		}
+		if (FD_ISSET(sockfd, &wfds)) {
+			int maxwrite = BLK_SIZE > io_buffers.in_len ? io_buffers.in_len : BLK_SIZE;
+			int nwritten = write(sockfd, io_buffers.in_buffer, maxwrite);
+			if (nwritten < 0) {
+				return 2;
+			} else {
+				memmove(io_buffers.in_buffer, io_buffers.in_buffer + nwritten, io_buffers.in_len - nwritten);
+				io_buffers.in_len -= nwritten;
+			}
+		}
+		if (FD_ISSET(outfd, &wfds)) {
+			int maxwrite = BLK_SIZE > io_buffers.out_len ? io_buffers.out_len : BLK_SIZE;
+			int nwritten = write(outfd, io_buffers.out_buffer, maxwrite);
+			if (nwritten < 0) {
+				return 2;
+			} else {
+				memmove(io_buffers.out_buffer, io_buffers.out_buffer + nwritten, io_buffers.out_len - nwritten);
+				io_buffers.out_len -= nwritten;
+			}
+		}
+	}
+
+	return 0;
+}
diff --git a/ltkd.c b/ltkd.c
@@ -0,0 +1,912 @@
+/*
+ * This file is part of the Lumidify ToolKit (LTK)
+ * Copyright (c) 2016, 2017, 2018, 2020 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <time.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include "util.h"
+#include "khash.h"
+#include "ini.h"
+#include "text.h"
+#include "ltk.h"
+#include "grid.h"
+#include "button.h"
+#include "draw.h"
+
+#define MAX_SOCK_CONNS 20
+#define READ_BLK_SIZE 128
+#define WRITE_BLK_SIZE 128
+
+struct token_list {
+	char **tokens;
+	int num_tokens;
+	int num_alloc;
+};
+
+static struct ltk_sock_info {
+	int fd;                    /* file descriptor for socket connection */
+	int event_mask;            /* events to send to socket */
+	char *read;                /* text read from socket */
+	int read_len;              /* length of text in read buffer */
+	int read_alloc;            /* size of read buffer */
+	char *to_write;            /* text to be written to socket */
+	int write_len;             /* length of text in write buffer */
+	int write_cur;             /* length of text already written */
+	int write_alloc;           /* size of write buffer */
+	/* stuff for tokenizing */
+	int in_token;              /* last read char is inside token */
+	int offset;                /* offset from removing backslashes */
+	int in_str;                /* last read char is inside string */
+	int read_cur;              /* length of text already tokenized */
+	int bs;                    /* last char was non-escaped backslash */
+	struct token_list tokens;  /* current tokens */
+} sockets[MAX_SOCK_CONNS];
+
+static void ltk_load_theme(ltk_window *window, const char *path);
+static void ltk_destroy_theme(ltk_theme *theme);
+static ltk_rect ltk_rect_union(ltk_rect r1, ltk_rect r2);
+static int read_sock(struct ltk_sock_info *sock);
+
+static int running = 1;
+
+static int
+push_token(struct token_list *tl, char *token) {
+	int new_size;
+	if (tl->num_tokens >= tl->num_alloc) {
+		new_size = (tl->num_alloc * 2) > (tl->num_tokens + 1) ?
+			   (tl->num_alloc * 2) : (tl->num_tokens + 1);
+		char **new = realloc(tl->tokens, new_size * sizeof(char *));
+		if (!new) return -1;
+		tl->tokens = new;
+		tl->num_alloc = new_size;
+	}
+	tl->tokens[tl->num_tokens++] = token;
+
+	return 0;
+}
+
+/* If `needed` is larger than `*alloc_size`, resize `*str` to `*alloc_size * 2`. */
+static int
+grow_string(char **str, int *alloc_size, int needed) {
+	if (needed <= *alloc_size) return 0;
+	int new_size = needed > (*alloc_size * 2) ? needed : (*alloc_size * 2);
+	char *new = realloc(*str, new_size);
+	if (!new) return 1;
+	*str = new;
+	*alloc_size = new_size;
+	return 0;
+}
+/* FIXME: non-blocking io? */
+/* Read up to READ_BLK_SIZE bytes from the socket.
+   Returns -1 if an error occurred, 0 if the connection was closed, 1 otherwise. */
+static int
+read_sock(struct ltk_sock_info *sock) {
+	int nread;
+	char *old = sock->read;
+	int ret = grow_string(&sock->read, &sock->read_alloc, sock->read_len + READ_BLK_SIZE);
+	if (ret) return -1; /* fixme: errno? */
+	/* move tokens to new addresses - this was added as an
+	   afterthought and really needs to be cleaned up */
+	if (sock->read != old) {
+		for (int i = 0; i < sock->tokens.num_tokens; i++) {
+			sock->tokens.tokens[i] = sock->read + (sock->tokens.tokens[i] - old);
+		}
+	}
+	nread = read(sock->fd, sock->read + sock->read_len, READ_BLK_SIZE);
+	if (nread == -1 || nread == 0)
+		return nread;
+	sock->read_len += nread;
+
+	return 1;
+}
+
+/* Write up to WRITE_BLK_SIZE bytes to the socket.
+   Returns -1 on error, 0 otherwise. */
+static int
+write_sock(struct ltk_sock_info *sock) {
+	if (!sock->write_len)
+		return 0;
+	int write_len = WRITE_BLK_SIZE > sock->write_len - sock->write_cur ?
+			sock->write_len - sock->write_cur : WRITE_BLK_SIZE;
+	int nwritten = write(sock->fd, sock->to_write + sock->write_cur, write_len);
+	if (nwritten == -1)
+		return nwritten;
+	sock->write_cur += nwritten;
+	return 0;
+}
+
+/* Queue str to be written to the socket. If len is < 0, it is set to strlen(str).
+   Returns -1 on error, 0 otherwise.
+   Note: The string must include all '\n', etc. as defined in the protocol. This
+   function just adds the given string verbatim. */
+static int
+queue_sock_write(struct ltk_sock_info *sock, const char *str, int len) {
+	if (sock->write_cur > 0) {
+		memmove(sock->to_write, sock->to_write + sock->write_cur,
+		        sock->write_len - sock->write_cur);
+		sock->write_len -= sock->write_cur;
+		sock->write_cur = 0;
+	}
+
+	if (len < 0)
+		len = strlen(str);
+
+	if (sock->write_alloc - sock->write_len < len &&
+	    grow_string(&sock->to_write, &sock->write_alloc, sock->write_len + len))
+		return -1;
+
+	(void)strncpy(sock->to_write + sock->write_len, str, len);
+	sock->write_len += len;
+
+	return 0;
+}
+
+/* Returns 0 if the end of a command was encountered, 1 otherwise */
+static int
+tokenize_command(struct ltk_sock_info *sock) {
+	for (; sock->read_cur < sock->read_len; sock->read_cur++) {
+		if (!sock->in_token) {
+			push_token(&sock->tokens, sock->read + sock->read_cur - sock->offset);
+			sock->in_token = 1;
+		}
+		if (sock->read[sock->read_cur] == '\\') {
+			sock->bs++;
+			sock->bs %= 2;
+			sock->read[sock->read_cur-sock->offset] = '\\';
+		} else if (sock->read[sock->read_cur] == '\n' && !sock->in_str) {
+			sock->read[sock->read_cur-sock->offset] = '\0';
+			sock->read_cur++;
+			sock->offset = 0;
+			sock->in_token = 0;
+			return 0;
+		} else if (sock->read[sock->read_cur] == '"') {
+			sock->offset++;
+			if (sock->bs) {
+				sock->read[sock->read_cur-sock->offset] = '"';
+				sock->bs = 0;
+			} else {
+				sock->in_str = !sock->in_str;
+			}
+		} else if (sock->read[sock->read_cur] == ' ' && !sock->in_str) {
+			sock->read[sock->read_cur-sock->offset] = '\0';
+			sock->in_token = !sock->in_token;
+		} else {
+			sock->read[sock->read_cur-sock->offset] = sock->read[sock->read_cur];
+			sock->bs = 0;
+		}
+	}
+
+	return 1;
+}
+
+void
+ltk_clean_up(ltk_window *window) {
+	ltk_destroy_theme(window->theme);
+	ltk_destroy_window(window);
+}
+
+void
+ltk_quit(ltk_window *window) {
+	ltk_clean_up(window);
+	running = 0;
+}
+
+static void
+ltk_set_root_widget_cmd(
+    ltk_window *window,
+    char **tokens,
+    int num_tokens) {
+	ltk_widget *widget;
+	if (num_tokens != 2) {
+		(void)fprintf(stderr, "set-root-widget: Invalid number of arguments.\n");
+		return;
+	}
+	widget = ltk_get_widget(window, tokens[1], LTK_WIDGET, "set-root-widget");
+	if (!widget) return;
+	window->root_widget = widget;
+	int w = widget->rect.w;
+	int h = widget->rect.h;
+	widget->rect.w = window->rect.w;
+	widget->rect.h = window->rect.h;
+	if (widget->resize) {
+		widget->resize(widget, w, h);
+	}
+}
+
+static void
+process_commands(ltk_window *window, struct ltk_sock_info *sock) {
+	char **tokens;
+	int num_tokens;
+	while (!tokenize_command(sock)) {
+		tokens = sock->tokens.tokens;
+		num_tokens = sock->tokens.num_tokens;
+		if (num_tokens < 1)
+			continue;
+		if (strcmp(tokens[0], "grid") == 0) {
+			ltk_grid_cmd(window, tokens, num_tokens);
+		} else if (strcmp(tokens[0], "button") == 0) {
+			ltk_button_cmd(window, tokens, num_tokens);
+		} else if (strcmp(tokens[0], "set-root-widget") == 0) {
+			ltk_set_root_widget_cmd(window, tokens, num_tokens);
+		} else if (strcmp(tokens[0], "draw") == 0) {
+			ltk_draw_cmd(window, tokens, num_tokens);
+		} else if (strcmp(tokens[0], "quit") == 0) {
+			ltk_quit(window);
+		} else {
+			/* FIXME... */
+			(void)fprintf(stderr, "Invalid command.\n");
+		}
+		sock->tokens.num_tokens = 0;
+	}
+	if (sock->tokens.num_tokens > 0 && sock->tokens.tokens[0] != sock->read) {
+		memmove(sock->read, sock->tokens.tokens[0], sock->read + sock->read_len - sock->tokens.tokens[0]);
+		ptrdiff_t offset = sock->tokens.tokens[0] - sock->read;
+		/* Hmm, seems a bit ugly... */
+		for (int i = 0; i < sock->tokens.num_tokens; i++) {
+			sock->tokens.tokens[i] -= offset;
+		}
+		sock->read_len -= offset;
+		sock->read_cur -= offset;
+	} else if (sock->tokens.num_tokens == 0) {
+		sock->read_len = 0;
+		sock->read_cur = 0;
+	}
+}
+
+static ltk_rect
+ltk_rect_union(ltk_rect r1, ltk_rect r2) {
+	ltk_rect u;
+	u.x = r1.x < r2.x ? r1.x : r2.x;
+	u.y = r1.y < r2.y ? r1.y : r2.y;
+	int x2 = r1.x + r1.w < r2.x + r2.w ? r2.x + r2.w : r1.x + r1.w;
+	int y2 = r1.y + r1.h < r2.y + r2.h ? r2.y + r2.h : r1.y + r1.h;
+	u.w = x2 - u.x;
+	u.h = y2 - u.y;
+	return u;
+}
+
+void
+ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect) {
+	if (window->dirty_rect.w == 0 && window->dirty_rect.h == 0)
+		window->dirty_rect = rect;
+	else
+		window->dirty_rect = ltk_rect_union(rect, window->dirty_rect);
+}
+
+void
+ltk_fatal(const char *msg) {
+	(void)fprintf(stderr, msg);
+	/* FIXME: clean up first */
+	exit(1);
+};
+
+int
+ltk_create_xcolor(ltk_window *window, const char *hex, XColor *col) {
+	if (!XParseColor(window->dpy, window->cm, hex, col)) {
+		(void)fprintf(stderr, "Invalid color: %s\n", hex);
+		return 0;
+	}
+	XAllocColor(window->dpy, window->cm, col);
+
+	return 1;
+}
+
+void
+ltk_queue_event(ltk_window *window, ltk_event_type type, const char *id, const char *data) {
+	/* FIXME: make it nicer and safer */
+	struct ltk_event_queue *new = malloc(sizeof(struct ltk_event_queue));
+	if (!new) ltk_fatal("Unable to queue event.\n");
+	new->event_type = type;
+	int id_len = strlen(id);
+	int data_len = strlen(data);
+	new->data = malloc(id_len + data_len + 3);
+	if (!new->data) ltk_fatal("Unable to queue event.\n");
+	strcpy(new->data, id);
+	new->data[id_len] = ' ';
+	strcpy(new->data + id_len + 1, data);
+	new->data[id_len + data_len + 1] = '\n';
+	new->data[id_len + data_len + 2] = '\0';
+	new->next = window->first_event;
+	window->first_event = new;
+	new->prev = NULL;
+	if (!window->last_event)
+		window->last_event = new;
+}
+
+static int
+add_client(int fd) {
+	for (int i = 0; i < MAX_SOCK_CONNS; i++) {
+		if (sockets[i].fd == -1) {
+			sockets[i].fd = fd;
+			sockets[i].event_mask = ~0; /* FIXME */
+			sockets[i].read_len = 0;
+			sockets[i].write_len = 0;
+			sockets[i].write_cur = 0;
+			sockets[i].offset = 0;
+			sockets[i].in_str = 0;
+			sockets[i].read_cur = 0;
+			sockets[i].bs = 0;
+			sockets[i].tokens.num_tokens = 0;
+			return i;
+		}
+	}
+
+	return -1;
+}
+
+/* largely copied from APUE */
+static int
+listen_sock(const char *sock_path) {
+	int fd, len, err, rval;
+	struct sockaddr_un un;
+
+	if (strlen(sock_path) >= sizeof(un.sun_path)) {
+		errno = ENAMETOOLONG;
+		return -1;
+	}
+
+	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
+		return -2;
+
+	unlink(sock_path);
+
+	memset(&un, 0, sizeof(un));
+	un.sun_family = AF_UNIX;
+	strcpy(un.sun_path, sock_path);
+	len = offsetof(struct sockaddr_un, sun_path) + strlen(sock_path);
+	if (bind(fd, (struct sockaddr *)&un, len) < 0) {
+		rval = -3;
+		goto errout;
+	}
+
+	if (listen(fd, 10) < 0) {
+		rval = -4;
+		goto errout;
+	}
+
+	return fd;
+
+errout:
+	err = errno;
+	close(fd);
+	errno = err;
+	return rval;
+}
+
+static int
+accept_sock(int listenfd) {
+	int clifd;
+	socklen_t len;
+	struct sockaddr_un un;
+
+	len = sizeof(un);
+	if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) {
+		return -1;
+	}
+
+	return clifd;
+}
+
+int
+ltk_mainloop(ltk_window *window) {
+	XEvent event;
+	fd_set rfds, wfds, rallfds, wallfds;
+	int maxi, maxfd, listenfd;
+	int rretval, wretval;
+	int clifd;
+	struct timeval tv;
+	tv.tv_sec = 0;
+	tv.tv_usec = 10;
+
+	/* Note: sockets should be initialized to 0 because it is static */
+	for (int i = 0; i < MAX_SOCK_CONNS; i++) {
+		sockets[i].fd = -1; /* socket unused */
+		/* initialize these just because I'm paranoid */
+		sockets[i].read = NULL;
+		sockets[i].to_write = NULL;
+		sockets[i].tokens.tokens = NULL;
+	}
+
+	FD_ZERO(&rallfds);
+	FD_ZERO(&wallfds);
+
+	if ((listenfd = listen_sock("ltk.sock")) < 0) {
+		fprintf(stderr, "Error listening on ltk.sock\n");
+		exit(1); /* FIXME: proper error handling */
+	}
+
+	FD_SET(listenfd, &rallfds);
+	maxfd = listenfd;
+	maxi = -1;
+
+	while (running) {
+		rfds = rallfds;
+		wfds = wallfds;
+		/* separate these because the writing fds are usually
+		   always ready for writing */
+		rretval = select(maxfd + 1, &rfds, NULL, NULL, &tv);
+		wretval = select(maxfd + 1, NULL, &wfds, NULL, &tv);
+		while (XPending(window->dpy)) {
+			XNextEvent(window->dpy, &event);
+			ltk_handle_event(window, event);
+		}
+		/* FIXME: somehow keep track of whether anything has to be written,
+		   otherwise it always has to loop over all fds to check - the writing
+		   fds are usually always set, so this is really run on every loop
+		   iteration, which is bad */
+		if (rretval > 0 || wretval > 0) {
+			if (FD_ISSET(listenfd, &rfds)) {
+				if ((clifd = accept_sock(listenfd)) < 0) {
+					fprintf(stderr, "Error accepting socket connection\n");
+					exit(1); /* FIXME: proper error handling */
+				}
+				int i = add_client(clifd);
+				FD_SET(clifd, &rallfds);
+				FD_SET(clifd, &wallfds);
+				if (clifd > maxfd)
+					maxfd = clifd;
+				if (i > maxi)
+					maxi = i;
+				continue;
+			}
+			for (int i = 0; i <= maxi; i++) {
+				if ((clifd = sockets[i].fd) < 0)
+					continue;
+				if (FD_ISSET(clifd, &rfds)) {
+					if (read_sock(&sockets[i]) == 0) {
+						fprintf(stderr, "Closed socket fd %d\n", clifd); /* FIXME */
+						FD_CLR(clifd, &rallfds);
+						FD_CLR(clifd, &wallfds);
+						sockets[i].fd = -1;
+						close(clifd);
+					} else {
+						process_commands(window, &sockets[i]);
+					}
+				}
+				if (FD_ISSET(clifd, &wfds)) {
+					write_sock(&sockets[i]);
+				}
+			}
+		}
+		if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) {
+			ltk_redraw_window(window);
+			window->dirty_rect.w = 0;
+			window->dirty_rect.h = 0;
+		}
+		if (window->last_event && running) {
+			struct ltk_event_queue *cur = window->last_event;
+			struct ltk_event_queue *last;
+			do {
+				int event_len = strlen(cur->data);
+				for (int i = 0; i <= maxi; i++) {
+					if (sockets[i].fd != -1 && sockets[i].event_mask & cur->event_type) {
+						if (queue_sock_write(&sockets[i], cur->data, event_len) < 0)
+							exit(1); /* FIXME: error handling */
+					}
+				}
+				free(cur->data);
+				last = cur;
+				cur = cur->prev;
+				free(last);
+			} while (cur);
+			window->first_event = window->last_event = NULL;
+		} 
+
+	}
+	for (int i = 0; i < MAX_SOCK_CONNS; i++) {
+		if (sockets[i].read)
+			free(sockets[i].read);
+		if (sockets[i].to_write)
+			free(sockets[i].to_write);
+		if (sockets[i].tokens.tokens)
+			free(sockets[i].tokens.tokens);
+	}
+
+	unlink("ltk.sock");
+
+	return 0;
+}
+
+void
+ltk_redraw_window(ltk_window *window) {
+	ltk_widget *ptr;
+	if (!window) return;
+	if (window->dirty_rect.x >= window->rect.w) return;
+	if (window->dirty_rect.y >= window->rect.h) return;
+	if (window->dirty_rect.x + window->dirty_rect.w > window->rect.w)
+		window->dirty_rect.w -= window->dirty_rect.x + window->dirty_rect.w - window->rect.w;
+	if (window->dirty_rect.y + window->dirty_rect.h > window->rect.h)
+		window->dirty_rect.h -= window->dirty_rect.y + window->dirty_rect.h - window->rect.h;
+	XClearArea(window->dpy, window->xwindow, window->dirty_rect.x, window->dirty_rect.y, window->dirty_rect.w, window->dirty_rect.h, False);
+	if (!window->root_widget) return;
+	/* FIXME: actually respect the dirty rect... */
+	ptr = window->root_widget;
+	ptr->draw(ptr);
+}
+
+void
+ltk_window_other_event(ltk_window *window, XEvent event) {
+	ltk_widget *ptr = window->root_widget;
+	if (event.type == ConfigureNotify) {
+		unsigned int w, h;
+		w = event.xconfigure.width;
+		h = event.xconfigure.height;
+		int orig_w = window->rect.w;
+		int orig_h = window->rect.h;
+		if (orig_w != w || orig_h != h) {
+			window->rect.w = w;
+			window->rect.h = h;
+			if (ptr && ptr->resize) {
+				ptr->rect.w = w;
+				ptr->rect.h = h;
+				ptr->resize(ptr, orig_w, orig_h);
+			}
+		}
+	} else if (event.type == Expose && event.xexpose.count == 0) {
+		ltk_rect r;
+		r.x = event.xexpose.x;
+		r.y = event.xexpose.y;
+		r.w = event.xexpose.width;
+		r.h = event.xexpose.height;
+		ltk_window_invalidate_rect(window, r);
+	} else if (event.type == ClientMessage
+	    && event.xclient.data.l[0] == window->wm_delete_msg) {
+		ltk_destroy_window(window);
+		exit(0); /* FIXME */
+	}
+}
+
+ltk_window *
+ltk_create_window(const char *theme_path, const char *title, int x, int y, unsigned int w, unsigned int h) {
+	ltk_window *window = malloc(sizeof(ltk_window));
+	if (!window)
+		ltk_fatal("Not enough memory left for window!\n");
+
+	window->dpy = XOpenDisplay(NULL);
+	window->screen = DefaultScreen(window->dpy);
+	window->cm = DefaultColormap(window->dpy, window->screen);
+	/* FIXME: validate theme properly */
+	ltk_load_theme(window, theme_path);
+	window->wm_delete_msg = XInternAtom(window->dpy, "WM_DELETE_WINDOW", False);
+
+	ltk_window_theme *wtheme = window->theme->window;
+	window->xwindow =
+	    XCreateSimpleWindow(window->dpy, DefaultRootWindow(window->dpy), x, y,
+				w, h, wtheme->border_width,
+				wtheme->fg.pixel, wtheme->bg.pixel);
+	window->gc = XCreateGC(window->dpy, window->xwindow, 0, 0);
+	XSetForeground(window->dpy, window->gc, wtheme->fg.pixel);
+	XSetBackground(window->dpy, window->gc, wtheme->bg.pixel);
+	XSetStandardProperties(window->dpy, window->xwindow, title, NULL, None,
+			       NULL, 0, NULL);
+	XSetWMProtocols(window->dpy, window->xwindow, &window->wm_delete_msg, 1);
+	window->root_widget = NULL;
+
+	ltk_init_text(wtheme->font, window->dpy, window->screen, window->cm);
+
+	window->other_event = <k_window_other_event;
+
+	window->rect.w = 0;
+	window->rect.h = 0;
+	window->rect.x = 0;
+	window->rect.y = 0;
+	window->dirty_rect.w = 0;
+	window->dirty_rect.h = 0;
+	window->dirty_rect.x = 0;
+	window->dirty_rect.y = 0;
+
+	window->widget_hash = kh_init(widget);
+
+	XClearWindow(window->dpy, window->xwindow);
+	XMapRaised(window->dpy, window->xwindow);
+	XSelectInput(window->dpy, window->xwindow,
+		     ExposureMask | KeyPressMask | KeyReleaseMask |
+		     ButtonPressMask | ButtonReleaseMask |
+		     StructureNotifyMask | PointerMotionMask);
+
+	return window;
+}
+
+void
+ltk_destroy_window(ltk_window *window) {
+	khint_t k;
+	ltk_widget *ptr;
+	XDestroyWindow(window->dpy, window->xwindow);
+	for (k = kh_begin(window->widget_hash); k != kh_end(window->widget_hash); k++) {
+		if (kh_exist(window->widget_hash, k)) {
+			ptr = kh_value(window->widget_hash, k);
+			ptr->destroy(ptr, 1);
+		}
+	}
+	kh_destroy(widget, window->widget_hash);
+	ltk_cleanup_text();
+	XCloseDisplay(window->dpy);
+	free(window);
+}
+
+void
+ltk_window_ini_handler(ltk_window *window, const char *prop, const char *value) {
+	/* FIXME: A whole lot more error checking! */
+	if (strcmp(prop, "border_width") == 0) {
+		window->theme->window->border_width = atoi(value);
+	} else if (strcmp(prop, "bg") == 0) {
+		ltk_create_xcolor(window, value, &window->theme->window->bg);
+	} else if (strcmp(prop, "fg") == 0) {
+		ltk_create_xcolor(window, value, &window->theme->window->fg);
+	} else if (strcmp(prop, "font") == 0) {
+		window->theme->window->font = strdup(value);
+	} else if (strcmp(prop, "font_size") == 0) {
+		window->theme->window->font_size = atoi(value);
+	}
+}
+
+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 {
+		return 0;
+	}
+	return 1;
+}
+
+static void
+ltk_load_theme(ltk_window *window, const char *path) {
+	window->theme = malloc(sizeof(ltk_theme));
+	if (!window->theme) ltk_fatal("Unable to allocate memory for theme.\n");
+	window->theme->window = malloc(sizeof(ltk_window_theme));
+	if (!window->theme->window) ltk_fatal("Unable to allocate memory for window theme.\n");
+	window->theme->button = NULL;
+	if (ini_parse(path, ltk_ini_handler, window) < 0) {
+		(void)fprintf(stderr, "ERROR: Can't load theme %s\n.", path);
+		exit(1);
+	}
+}
+
+static void
+ltk_destroy_theme(ltk_theme *theme) {
+	free(theme->button);
+	free(theme->window);
+	free(theme);
+}
+
+int
+ltk_collide_rect(ltk_rect rect, int x, int y) {
+	return (rect.x <= x && (rect.x + rect.w) >= x && rect.y <= y
+		&& (rect.y + rect.h) >= y);
+}
+
+void
+ltk_window_remove_active_widget(ltk_window *window) {
+	ltk_widget *widget = window->active_widget;
+	if (!widget) return;
+	while (widget) {
+		widget->state = LTK_NORMAL;
+		widget->active_widget = NULL;
+		if (widget->change_state)
+			widget->change_state(widget);
+		if (widget->needs_redraw)
+			ltk_window_invalidate_rect(window, widget->rect);
+		widget = widget->parent;
+	}
+	window->active_widget = NULL;
+}
+
+void
+ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
+	window->active_widget = widget;
+	ltk_widget *parent = widget->parent;
+	widget->state = LTK_ACTIVE;
+	while (parent) {
+		widget->state = LTK_ACTIVE;
+		parent->active_widget = widget;
+		widget = parent;
+		parent = widget->parent;
+	}
+}
+
+void
+ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window *window,
+    void (*draw) (void *), void (*change_state) (void *),
+    void (*destroy) (void *, int), unsigned int needs_redraw,
+    ltk_widget_type type) {
+	widget->id = strdup(id);
+	widget->window = window;
+	widget->active_widget = NULL;
+	widget->parent = NULL;
+	widget->type = type;
+
+	widget->key_press = NULL;
+	widget->key_release = NULL;
+	widget->mouse_press = NULL;
+	widget->mouse_release = NULL;
+	widget->motion_notify = NULL;
+	widget->mouse_enter = NULL;
+	widget->mouse_leave = NULL;
+
+	widget->resize = NULL;
+	widget->draw = draw;
+	widget->change_state = change_state;
+	widget->destroy = destroy;
+
+	widget->needs_redraw = needs_redraw;
+	widget->state = LTK_NORMAL;
+	widget->row = 0;
+	widget->rect.x = 0;
+	widget->rect.y = 0;
+	widget->rect.w = 100;
+	widget->rect.h = 100;
+
+	widget->row = 0;
+	widget->column = 0;
+	widget->row_span = 0;
+	widget->column_span = 0;
+	widget->sticky = 0;
+}
+
+void
+ltk_widget_mouse_press_event(ltk_widget *widget, XEvent event) {
+	if (!widget || widget->state == LTK_DISABLED)
+		return;
+	if (event.xbutton.button == 1) {
+		/* ltk_widget *parent = widget->parent; FIXME */
+		widget->state = LTK_PRESSED;
+		if (widget->change_state)
+			widget->change_state(widget);
+		if (widget->needs_redraw)
+			ltk_window_invalidate_rect(widget->window, widget->rect);
+	}
+	if (widget->mouse_press) {
+		widget->mouse_press(widget, event);
+	}
+}
+
+void
+ltk_widget_mouse_release_event(ltk_widget *widget, XEvent event) {
+	if (!widget || widget->state == LTK_DISABLED)
+		return;
+	if (widget->state == LTK_PRESSED) {
+		widget->state = LTK_ACTIVE;
+		if (widget->change_state)
+			widget->change_state(widget);
+		if (widget->needs_redraw)
+			ltk_window_invalidate_rect(widget->window, widget->rect);
+	}
+	if (widget->mouse_release) {
+		widget->mouse_release(widget, event);
+	}
+}
+
+void
+ltk_widget_motion_notify_event(ltk_widget *widget, XEvent event) {
+	if (!widget) return;
+	short pressed = (event.xmotion.state & Button1Mask) == Button1Mask;
+	if ((widget->state == LTK_NORMAL) && !pressed) {
+		widget->state = LTK_ACTIVE;
+		if (widget->change_state)
+			widget->change_state(widget);
+		if (widget->mouse_enter)
+			widget->mouse_enter(widget, event);
+		/* FIXME: do this properly */
+		if (widget->window->active_widget != widget && !widget->motion_notify) {
+			ltk_window_remove_active_widget(widget->window);
+			ltk_window_set_active_widget(widget->window, widget);
+		}
+		if (widget->needs_redraw)
+			ltk_window_invalidate_rect(widget->window, widget->rect);
+	}
+	if (widget->motion_notify)
+		widget->motion_notify(widget, event);
+}
+
+void
+ltk_handle_event(ltk_window *window, XEvent event) {
+	ltk_widget *root_widget = window->root_widget;
+	switch (event.type) {
+	case KeyPress:
+		break;
+	case KeyRelease:
+		break;
+	case ButtonPress:
+		if (root_widget)
+			ltk_widget_mouse_press_event(root_widget, event);
+		break;
+	case ButtonRelease:
+		if (root_widget)
+			ltk_widget_mouse_release_event(root_widget, event);
+		break;
+	case MotionNotify:
+		if (root_widget)
+			ltk_widget_motion_notify_event(root_widget, event);
+		break;
+	default:
+		if (window->other_event)
+			window->other_event(window, event);
+	}
+}
+
+int
+ltk_check_widget_id_free(ltk_window *window, const char *id, const char *caller) {
+	khint_t k;
+	k = kh_get(widget, window->widget_hash, id);
+	if (k != kh_end(window->widget_hash)) {
+		(void)fprintf(stderr, "%s: Widget \"%s\" already exists.\n", caller, id);
+		return 0;
+	}
+	return 1;
+}
+
+ltk_widget *
+ltk_get_widget(ltk_window *window, const char *id, ltk_widget_type type,
+    const char *caller) {
+	khint_t k;
+	ltk_widget *widget;
+	k = kh_get(widget, window->widget_hash, id);
+	if (k == kh_end(window->widget_hash)) {
+		(void)fprintf(stderr, "%s: Widget \"%s\" doesn't exist.\n", caller, id);
+		return NULL;
+	}
+	widget = kh_value(window->widget_hash, k);
+	if (type != LTK_WIDGET && widget->type != type) {
+		(void)fprintf(stderr, "%s: Widget \"%s\" has wrong type.\n", caller, id);
+		return NULL;
+	}
+	return widget;
+}
+
+void
+ltk_set_widget(ltk_window *window, ltk_widget *widget, const char *id) {
+	int ret;
+	khint_t k;
+	/* apparently, khash requires the string to stay accessible */
+	char *tmp = strdup(id);
+	k = kh_put(widget, window->widget_hash, tmp, &ret);
+	kh_value(window->widget_hash, k) = widget;
+}
+
+void
+ltk_remove_widget(ltk_window *window, const char *id) {
+	khint_t k;
+	k = kh_get(widget, window->widget_hash, id);
+	if (k != kh_end(window->widget_hash)) {
+		kh_del(widget, window->widget_hash, k);
+	}
+}
+
+int main(int argc, char *argv[]) {
+	ltk_window *window = ltk_create_window("theme.ini", "Demo", 0, 0, 500, 500);
+	return ltk_mainloop(window);
+}
diff --git a/socket_format.txt b/socket_format.txt
@@ -1,5 +1,5 @@
-Note: This is not implemented yet; it is just here to collect
-my thoughts while I keep working.
+Note: This is not fully implemented yet; it is just here to
+collect my thoughts while I keep working.
 
 <widget type> <widget id> <command> <args>
 > grid grd1 create 2 2
diff --git a/test.sh b/test.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+# This is very hacky.
+#
+# All events are still printed to the terminal curerntly because
+# the second './ltkc' still prints everything - event masks aren't
+# supported yet.
+#
+# Currently, everything just uses the socket 'ltk.sock'. My idea
+# was to have a directory containing sockets for all instances of
+# ltks, each of them named after the X Window ID used. This would
+# allow other tools (screenreader, etc.) to determine which socket
+# to use to communicate with a window. Probably ltkd would just
+# print out its window ID when starting and then daemonize itself,
+# so the calling script can just use save the output of ltkd and
+# use that to communicate with the socket using ltkc.
+
+./ltkd&
+sleep 0.2
+
+cat test.gui | ./ltkc | while read cmd
+do
+	case "$cmd" in
+	"btn1 button_click") echo "quit"
+	;;
+	esac
+done | ./ltkc
diff --git a/test_anim.sh b/test_anim.sh
@@ -1,29 +0,0 @@
-#!/bin/sh
-
-echo "grid grd1 create 2 2
-grid grd1 set-row-weight 0 1
-grid grd1 set-row-weight 1 1
-grid grd1 set-column-weight 0 1
-grid grd1 set-column-weight 1 1
-set-root-widget grd1
-draw drw1 create 100 100 #fff
-grid grd1 add drw1 0 1 1 1 15
-draw drw1 set-color #000
-"
-
-i=0
-while [ $i -lt 400 ]; do
-	j=0
-	while [ $j -lt 20 ]; do
-		echo "draw drw1 line $((i+j)) $j $((i+j+2)) $((j+2))";
-		sleep 0.05
-		j=$((j+2))
-	done
-	j=20
-	while [ $j -gt 0 ]; do
-		echo "draw drw1 line $((i+40-j)) $j $((i+42-j)) $((j-2))";
-		sleep 0.05
-		j=$((j-2))
-	done
-	i=$((i+40));
-done
diff --git a/test_draw.gui b/test_draw.gui
@@ -1,10 +0,0 @@
-grid grd1 create 2 2
-grid grd1 set-row-weight 0 1
-grid grd1 set-row-weight 1 1
-grid grd1 set-column-weight 0 1
-grid grd1 set-column-weight 1 1
-set-root-widget grd1
-draw drw1 create 100 100 #fff
-grid grd1 add drw1 0 1 1 1 15
-draw drw1 set-color #000
-draw drw1 line 0 0 100 100