commit a4ab655cc97c6c7f7383e9567b413fd34819bb42
Author: lumidify <nobody@lumidify.org>
Date:   Mon,  1 Jun 2020 21:14:20 +0200
Initial commit
Diffstat:
| A | .gitignore |  |  | 3 | +++ | 
| A | LICENSE |  |  | 22 | ++++++++++++++++++++++ | 
| A | Makefile |  |  | 15 | +++++++++++++++ | 
| A | README.md |  |  | 1 | + | 
| A | button.c |  |  | 131 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | button.h |  |  | 58 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | grid.c |  |  | 270 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | grid.h |  |  | 122 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | ini.c |  |  | 201 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | ini.h |  |  | 104 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | khash.h |  |  | 627 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | ltk.c |  |  | 563 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | ltk.h |  |  | 158 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | test.sh |  |  | 6 | ++++++ | 
| A | theme.ini |  |  | 18 | ++++++++++++++++++ | 
15 files changed, 2299 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+ltk
+*.o
+*.core
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,22 @@
+MIT/X Consortium License
+
+The Lumidify ToolKit (LTK)
+Copyright (c) 2016, 2017, 2018, 2019, 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.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,15 @@
+LIBS = -lm `pkg-config --libs x11`
+STD = -std=c99
+CFLAGS = -g -w -fcommon -Wall -Werror -Wextra `pkg-config --cflags x11` -pedantic
+OBJ = ltk.o ini.o grid.o button.o
+
+ltk: $(OBJ)
+	$(CC) $(STD) -o $@ $(OBJ) $(LIBS)
+
+%.o: %.c
+	$(CC) -c -o $@ $< $(CFLAGS)
+
+.PHONY: clean
+
+clean:
+	rm -f $(OBJ) ltk ltk_in
diff --git a/README.md b/README.md
@@ -0,0 +1 @@
+Not much to see here.
diff --git a/button.c b/button.c
@@ -0,0 +1,131 @@
+/*
+ * This file is part of the Lumidify ToolKit (LTK)
+ * Copyright (c) 2016, 2017, 2018 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 <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include "khash.h"
+#include "ltk.h"
+#include "button.h"
+
+void
+ltk_button_ini_handler(ltk_window *window, const char *prop, const char *value) {
+	ltk_theme *theme = window->theme;
+	if (!theme->button)
+		theme->button = malloc(sizeof(ltk_button_theme));
+	if (!theme->button)
+		ltk_fatal("Unable to allocate ltk_button_theme.\n");
+	if (strcmp(prop, "border_width") == 0) {
+		theme->button->border_width = atoi(value);
+	} else if (strcmp(prop, "pad") == 0) {
+		theme->button->pad = atoi(value);
+	} else if (strcmp(prop, "border") == 0) {
+		theme->button->border = ltk_create_xcolor(window, value);
+	} else if (strcmp(prop, "fill") == 0) {
+		theme->button->fill = ltk_create_xcolor(window, value);
+	} else if (strcmp(prop, "border_pressed") == 0) {
+		theme->button->border_pressed = ltk_create_xcolor(window, value);
+	} else if (strcmp(prop, "fill_pressed") == 0) {
+		theme->button->fill_pressed = ltk_create_xcolor(window, value);
+	} else if (strcmp(prop, "border_active") == 0) {
+		theme->button->border_active = ltk_create_xcolor(window, value);
+	} else if (strcmp(prop, "fill_active") == 0) {
+		theme->button->fill_active = ltk_create_xcolor(window, value);
+	} else if (strcmp(prop, "border_disabled") == 0) {
+		theme->button->border_disabled = ltk_create_xcolor(window, value);
+	} else if (strcmp(prop, "fill_disabled") == 0) {
+		theme->button->fill_disabled = ltk_create_xcolor(window, value);
+	} else if (strcmp(prop, "text_color") == 0) {
+		theme->button->text_color = ltk_create_xcolor(window, value);
+	} else {
+		(void)printf("WARNING: Unknown property \"%s\" for button style.\n", prop);
+	}
+}
+
+void
+ltk_button_draw(ltk_button *button) {
+	ltk_window *window = button->widget.window;
+	ltk_button_theme *theme = window->theme->button;
+	ltk_rect rect = button->widget.rect;
+	int bw = theme->border_width;
+	XColor border;
+	XColor fill;
+	switch (button->widget.state) {
+	case LTK_NORMAL:
+		border = theme->border;
+		fill = theme->fill;
+		break;
+	case LTK_PRESSED:
+		border = theme->border_pressed;
+		fill = theme->fill_pressed;
+		break;
+	case LTK_ACTIVE:
+		border = theme->border_active;
+		fill = theme->fill_active;
+		break;
+	case LTK_DISABLED:
+		border = theme->border_disabled;
+		fill = theme->fill_disabled;
+		break;
+	default:
+		ltk_fatal("No style found for button!\n");
+	}
+	XSetForeground(window->dpy, window->gc, fill.pixel);
+	XFillRectangle(window->dpy, window->xwindow, window->gc, rect.x, rect.y, rect.w, rect.h);
+	/* FIXME: Why did I do this? */
+	if (bw < 1) return;
+	XSetForeground(window->dpy, window->gc, border.pixel);
+	XSetLineAttributes(window->dpy, window->gc, bw, LineSolid, CapButt, JoinMiter);
+	XDrawRectangle(window->dpy, window->xwindow, window->gc, rect.x + bw / 2, rect.y + bw / 2, rect.w - bw, rect.h - bw);
+}
+
+
+void
+ltk_button_mouse_release(ltk_button *button, XEvent event) {
+	ltk_queue_event(button->widget.window, button->widget.id, "button_click");
+}
+
+ltk_button *
+ltk_button_create(ltk_window *window, const char *id, const char *text) {
+	ltk_button *button = malloc(sizeof(ltk_button));
+	if (!button) ltk_fatal("ERROR: Unable to allocate memory for ltk_button.\n");
+
+	ltk_fill_widget_defaults(&button->widget, id, window, <k_button_draw, <k_button_destroy, 1);
+	button->widget.mouse_release = <k_button_mouse_release;
+	button->text = strdup(text);
+
+	return button;
+}
+
+void
+ltk_button_destroy(ltk_button *button) {
+	if (!button) {
+		(void)printf("WARNING: Tried to destroy NULL button.\n");
+		return;
+	}
+	free(button->text);
+	free(button->widget.id);
+	free(button);
+}
diff --git a/button.h b/button.h
@@ -0,0 +1,58 @@
+/*
+ * This file is part of the Lumidify ToolKit (LTK)
+ * Copyright (c) 2016, 2017, 2018 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_BUTTON_H_
+#define _LTK_BUTTON_H_
+
+/* Requires the following includes: <X11/Xlib.h>, "ltk.h" */
+
+typedef struct {
+	ltk_widget widget;
+	char *text;
+} ltk_button;
+
+typedef struct ltk_button_theme {
+	int border_width;
+	XColor text_color;
+	int pad;
+
+	XColor border;
+	XColor fill;
+
+	XColor border_pressed;
+	XColor fill_pressed;
+
+	XColor border_active;
+	XColor fill_active;
+
+	XColor border_disabled;
+	XColor fill_disabled;
+} ltk_button_theme;
+
+void ltk_button_draw(ltk_button *button);
+
+ltk_button *ltk_button_create(ltk_window *window, const char *id, const char *text);
+
+void ltk_button_destroy(ltk_button *button);
+
+#endif
diff --git a/grid.c b/grid.c
@@ -0,0 +1,270 @@
+/*
+ * 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.
+ */
+
+/* TODO: remove_widget function that also adjusts static width */
+/* TODO: widget size request */
+
+#include <stdlib.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include "khash.h"
+#include "ltk.h"
+#include "grid.h"
+
+void ltk_set_row_weight(ltk_grid * grid, int row, int weight) {
+	grid->row_weights[row] = weight;
+	ltk_recalculate_grid(grid);
+}
+
+void ltk_set_column_weight(ltk_grid * grid, int column, int weight) {
+	grid->column_weights[column] = weight;
+	ltk_recalculate_grid(grid);
+}
+
+void ltk_draw_grid(ltk_grid *grid) {
+	int i;
+	for (i = 0; i < grid->rows * grid->columns; i++) {
+		if (!grid->widget_grid[i])
+			continue;
+		ltk_widget *ptr = grid->widget_grid[i];
+		ptr->draw(ptr);
+	}
+}
+
+ltk_grid *ltk_create_grid(ltk_window *window, const char *id, int rows, int columns) {
+	ltk_grid *grid = malloc(sizeof(ltk_grid));
+
+	ltk_fill_widget_defaults(&grid->widget, id, window, <k_draw_grid, <k_destroy_grid, 0);
+	grid->widget.mouse_press = <k_grid_mouse_press;
+	grid->widget.mouse_release = <k_grid_mouse_release;
+	grid->widget.motion_notify = <k_grid_motion_notify;
+	grid->widget.resize = <k_recalculate_grid;
+
+	grid->rows = rows;
+	grid->columns = columns;
+	grid->widget_grid = malloc(rows * columns * sizeof(ltk_widget));
+	grid->row_heights = malloc(rows * sizeof(int));
+	grid->column_widths = malloc(rows * sizeof(int));
+	grid->row_weights = malloc(rows * sizeof(int));
+	grid->column_weights = malloc(columns * sizeof(int));
+	/* Positions have one extra for the end */
+	grid->row_pos = malloc((rows + 1) * sizeof(int));
+	grid->column_pos = malloc((columns + 1) * sizeof(int));
+	int i;
+	for (i = 0; i < rows; i++) {
+		grid->row_heights[i] = 0;
+		grid->row_weights[i] = 0;
+		grid->row_pos[i] = 0;
+	}
+	grid->row_pos[rows] = 0;
+	for (i = 0; i < columns; i++) {
+		grid->column_widths[i] = 0;
+		grid->column_weights[i] = 0;
+		grid->column_pos[i] = 0;
+	}
+	grid->column_pos[columns] = 0;
+	for (i = 0; i < rows * columns; i++) {
+		grid->widget_grid[i] = NULL;
+	}
+
+	ltk_recalculate_grid(grid);
+	return grid;
+}
+
+void ltk_destroy_grid(ltk_grid *grid) {
+	ltk_widget *ptr;
+	int i;
+	for (i = 0; i < grid->rows * grid->columns; i++) {
+		if (grid->widget_grid[i]) {
+			ptr = grid->widget_grid[i];
+			ptr->destroy(ptr);
+		}
+	}
+	free(grid->widget_grid);
+	free(grid->row_heights);
+	free(grid->column_widths);
+	free(grid->row_weights);
+	free(grid->column_weights);
+	free(grid->row_pos);
+	free(grid->column_pos);
+	free(grid);
+}
+
+void ltk_recalculate_grid(ltk_grid *grid) {
+	unsigned int height_static = 0, width_static = 0;
+	unsigned int total_row_weight = 0, total_column_weight = 0;
+	float height_unit = 0, width_unit = 0;
+	unsigned int currentx = 0, currenty = 0;
+	int i, j;
+	for (i = 0; i < grid->rows; i++) {
+		total_row_weight += grid->row_weights[i];
+		if (grid->row_weights[i] == 0) {
+			height_static += grid->row_heights[i];
+		}
+	}
+	for (i = 0; i < grid->columns; i++) {
+		total_column_weight += grid->column_weights[i];
+		if (grid->column_weights[i] == 0) {
+			width_static += grid->column_widths[i];
+		}
+	}
+	if (total_row_weight > 0) {
+		height_unit = (float) (grid->widget.rect.h - height_static) / (float) total_row_weight;
+	}
+	if (total_column_weight > 0) {
+		width_unit = (float) (grid->widget.rect.w - width_static) / (float) total_column_weight;
+	}
+	for (i = 0; i < grid->rows; i++) {
+		grid->row_pos[i] = currenty;
+		if (grid->row_weights[i] > 0) {
+			grid->row_heights[i] = grid->row_weights[i] * height_unit;
+		}
+		currenty += grid->row_heights[i];
+	}
+	grid->row_pos[grid->rows] = currenty;
+	for (i = 0; i < grid->columns; i++) {
+		grid->column_pos[i] = currentx;
+		if (grid->column_weights[i] > 0) {
+			grid->column_widths[i] = grid->column_weights[i] * width_unit;
+		}
+		currentx += grid->column_widths[i];
+	}
+	grid->column_pos[grid->columns] = currentx;
+	int orig_width, orig_height;
+	int end_column, end_row;
+	for (i = 0; i < grid->rows; i++) {
+		for (j = 0; j < grid->columns; j++) {
+			if (!grid->widget_grid[i * grid->columns + j]) {
+				continue;
+			}
+			ltk_widget *ptr = grid->widget_grid[i * grid->columns + j];
+			orig_width = ptr->rect.w;
+			orig_height = ptr->rect.h;
+			end_row = i + ptr->row_span;
+			end_column = j + ptr->column_span;
+			if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT) {
+				ptr->rect.w = grid->column_pos[end_column] - grid->column_pos[j];
+			}
+			if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM) {
+				ptr->rect.h = grid->row_pos[end_row] - grid->row_pos[i];
+			}
+			if (orig_width != ptr->rect.w || orig_height != ptr->rect.h) {
+				if (ptr->resize) {
+					ptr->resize(ptr, orig_width, orig_height);
+				}
+			}
+
+			if (ptr->sticky & LTK_STICKY_RIGHT) {
+				ptr->rect.x = grid->column_pos[end_column] - ptr->rect.w;
+			} else if (ptr->sticky & LTK_STICKY_LEFT) {
+				ptr->rect.x = grid->column_pos[j];
+			} else {
+				ptr->rect.x = grid->column_pos[j] + ((grid->column_pos[end_column] - grid->column_pos[j]) / 2 - ptr->rect.w / 2);
+			}
+
+			if (ptr->sticky & LTK_STICKY_BOTTOM) {
+				ptr->rect.y = grid->row_pos[end_row] - ptr->rect.h;
+			} else if (ptr->sticky & LTK_STICKY_TOP) {
+				ptr->rect.y = grid->row_pos[i];
+			} else {
+				ptr->rect.y = grid->row_pos[i] + ((grid->row_pos[end_row] - grid->row_pos[i]) / 2 - ptr->rect.h / 2);
+			}
+		}
+	}
+}
+
+void ltk_grid_widget(ltk_widget *widget, ltk_grid *grid, int row, int column, int row_span, int column_span, unsigned short sticky) {
+	widget->sticky = sticky;
+	widget->row = row;
+	widget->column = column;
+	widget->row_span = row_span;
+	widget->column_span = column_span;
+	if (grid->column_weights[column] == 0 && widget->rect.w > grid->column_widths[column]) {
+		grid->column_widths[column] = widget->rect.w;
+	}
+	if (grid->row_weights[row] == 0 && widget->rect.h > grid->row_heights[row]) {
+		grid->row_heights[row] = widget->rect.h;
+	}
+	grid->widget_grid[widget->row * grid->columns + widget->column] = widget;
+	widget->parent = grid;
+	ltk_recalculate_grid(grid);
+}
+
+static int ltk_grid_find_nearest_column(ltk_grid *grid, int x) {
+	int i;
+	for (i = 0; i < grid->columns; i++) {
+		if (grid->column_pos[i] <= x && grid->column_pos[i + 1] >= x) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+static int ltk_grid_find_nearest_row(ltk_grid *grid, int y) {
+	int i;
+	for (i = 0; i < grid->rows; i++) {
+		if (grid->row_pos[i] <= y && grid->row_pos[i + 1] >= y) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+void ltk_grid_mouse_press(ltk_grid *grid, XEvent event) {
+	int x = event.xbutton.x;
+	int y = event.xbutton.y;
+	int row = ltk_grid_find_nearest_row(grid, y);
+	int column = ltk_grid_find_nearest_column(grid, x);
+	if (row == -1 || column == -1)
+		return;
+	ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
+	if (ptr && ltk_collide_rect(ptr->rect, x, y))
+		ltk_widget_mouse_press_event(ptr, event);
+}
+
+void ltk_grid_mouse_release(ltk_grid *grid, XEvent event) {
+	int x = event.xbutton.x;
+	int y = event.xbutton.y;
+	int row = ltk_grid_find_nearest_row(grid, y);
+	int column = ltk_grid_find_nearest_column(grid, x);
+	if (row == -1 || column == -1)
+		return;
+	ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
+	if (ptr && ltk_collide_rect(ptr->rect, x, y))
+		ltk_widget_mouse_release_event(ptr, event);
+}
+
+void ltk_grid_motion_notify(ltk_grid *grid, XEvent event) {
+	short pressed = (event.xmotion.state & Button1Mask) == Button1Mask;
+	if (pressed)
+		return;
+	int x = event.xbutton.x;
+	int y = event.xbutton.y;
+	int row = ltk_grid_find_nearest_row(grid, y);
+	int column = ltk_grid_find_nearest_column(grid, x);
+	if (row == -1 || column == -1)
+		return;
+	ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
+	if (ptr && ltk_collide_rect(ptr->rect, x, y))
+		ltk_widget_motion_notify_event(ptr, event);
+}
diff --git a/grid.h b/grid.h
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+#ifndef _LTK_GRID_H_
+#define _LTK_GRID_H_
+
+/* Requires the following includes: <X11/Xlib.h>, "ltk.h" */
+
+/*
+ * Struct to represent a grid widget.
+ */
+typedef struct {
+	ltk_widget widget;
+	unsigned int rows;
+	unsigned int columns;
+	ltk_widget **widget_grid;
+	unsigned int *row_heights;
+	unsigned int *column_widths;
+	unsigned int *row_weights;
+	unsigned int *column_weights;
+	unsigned int *row_pos;
+	unsigned int *column_pos;
+} ltk_grid;
+
+/*
+ * Set the weight of a row in a grid.
+ * grid: The grid.
+ * row: The row.
+ * weight: The weight to set the row to.
+ */
+void ltk_set_row_weight(ltk_grid *grid, int row, int weight);
+
+/*
+ * Set the weight of a column in a grid.
+ * grid: The grid.
+ * column: The column.
+ * weight: The weight to set the row to.
+ */
+void ltk_set_column_weight(ltk_grid *grid, int column, int weight);
+
+/*
+ * Draw all the widgets in a grid.
+ * grid: The grid to draw the widgets of.
+ */
+void ltk_draw_grid(ltk_grid *grid);
+
+/*
+ * Create a grid.
+ * window: The window the grid will displayed on.
+ * rows: The number of rows in the grid.
+ * columns: The number of columns in the grid.
+ */
+ltk_grid *ltk_create_grid(ltk_window *window, const char *id, int rows, int columns);
+
+/*
+ * Destroy a grid.
+ * grid: Pointer to the grid.
+ */
+void ltk_destroy_grid(ltk_grid *grid);
+
+/*
+ * Recalculate the positions and dimensions of the
+ * columns, rows, and widgets in a grid.
+ * grid: Pointer to the grid.
+ */
+void ltk_recalculate_grid(ltk_grid *grid);
+
+/*
+ * Grid a widget.
+ * widget: Pointer to the widget.
+ * grid: The grid.
+ * row: The row to grid the widget in.
+ * column: The column to grid the widget in.
+ * rowspan: The amount of rows the widget should span.
+ * columnspan: The amount of columns the widget should span.
+ * sticky: Mask of the sticky values (LTK_STICKY_*).
+ */
+void ltk_grid_widget(ltk_widget *widget, ltk_grid *grid, int row, int column,
+		     int rowspan, int columnspan, unsigned short sticky);
+
+/*
+ * Delegate a mouse press event on the grid to the proper widget.
+ * grid: The grid.
+ * event: The event to be handled.
+ */
+void ltk_grid_mouse_press(ltk_grid *grid, XEvent event);
+
+/*
+ * Delegate a mouse release event on the grid to the proper widget.
+ * grid: The grid.
+ * event: The event to be handled.
+ */
+void ltk_grid_mouse_release(ltk_grid *grid, XEvent event);
+
+/*
+ * Delegate a mouse motion event on the grid to the proper widget.
+ * grid: The grid.
+ * event: The event to be handled.
+ */
+void ltk_grid_motion_notify(ltk_grid *grid, XEvent event);
+
+#endif
diff --git a/ini.c b/ini.c
@@ -0,0 +1,201 @@
+/* inih -- simple .INI file parser
+
+inih is released under the New BSD license (see LICENSE.txt). Go to the project
+home page for more info:
+
+https://github.com/benhoyt/inih
+
+*/
+
+#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
+#define _CRT_SECURE_NO_WARNINGS
+#endif
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+
+#include "ini.h"
+
+#if !INI_USE_STACK
+#include <stdlib.h>
+#endif
+
+#define MAX_SECTION 50
+#define MAX_NAME 50
+
+/* Strip whitespace chars off end of given string, in place. Return s. */
+static char* rstrip(char* s)
+{
+    char* p = s + strlen(s);
+    while (p > s && isspace((unsigned char)(*--p)))
+        *p = '\0';
+    return s;
+}
+
+/* Return pointer to first non-whitespace char in given string. */
+static char* lskip(const char* s)
+{
+    while (*s && isspace((unsigned char)(*s)))
+        s++;
+    return (char*)s;
+}
+
+/* Return pointer to first char (of chars) or inline comment in given string,
+   or pointer to null at end of string if neither found. Inline comment must
+   be prefixed by a whitespace character to register as a comment. */
+static char* find_chars_or_comment(const char* s, const char* chars)
+{
+#if INI_ALLOW_INLINE_COMMENTS
+    int was_space = 0;
+    while (*s && (!chars || !strchr(chars, *s)) &&
+           !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
+        was_space = isspace((unsigned char)(*s));
+        s++;
+    }
+#else
+    while (*s && (!chars || !strchr(chars, *s))) {
+        s++;
+    }
+#endif
+    return (char*)s;
+}
+
+/* Version of strncpy that ensures dest (size bytes) is null-terminated. */
+static char* strncpy0(char* dest, const char* src, size_t size)
+{
+    strncpy(dest, src, size);
+    dest[size - 1] = '\0';
+    return dest;
+}
+
+/* See documentation in header file. */
+int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
+                     void* user)
+{
+    /* Uses a fair bit of stack (use heap instead if you need to) */
+#if INI_USE_STACK
+    char line[INI_MAX_LINE];
+#else
+    char* line;
+#endif
+    char section[MAX_SECTION] = "";
+    char prev_name[MAX_NAME] = "";
+
+    char* start;
+    char* end;
+    char* name;
+    char* value;
+    int lineno = 0;
+    int error = 0;
+
+#if !INI_USE_STACK
+    line = (char*)malloc(INI_MAX_LINE);
+    if (!line) {
+        return -2;
+    }
+#endif
+
+#if INI_HANDLER_LINENO
+#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)
+#else
+#define HANDLER(u, s, n, v) handler(u, s, n, v)
+#endif
+
+    /* Scan through stream line by line */
+    while (reader(line, INI_MAX_LINE, stream) != NULL) {
+        lineno++;
+
+        start = line;
+#if INI_ALLOW_BOM
+        if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
+                           (unsigned char)start[1] == 0xBB &&
+                           (unsigned char)start[2] == 0xBF) {
+            start += 3;
+        }
+#endif
+        start = lskip(rstrip(start));
+
+        if (*start == ';' || *start == '#') {
+            /* Per Python configparser, allow both ; and # comments at the
+               start of a line */
+        }
+#if INI_ALLOW_MULTILINE
+        else if (*prev_name && *start && start > line) {
+            /* Non-blank line with leading whitespace, treat as continuation
+               of previous name's value (as per Python configparser). */
+            if (!HANDLER(user, section, prev_name, start) && !error)
+                error = lineno;
+        }
+#endif
+        else if (*start == '[') {
+            /* A "[section]" line */
+            end = find_chars_or_comment(start + 1, "]");
+            if (*end == ']') {
+                *end = '\0';
+                strncpy0(section, start + 1, sizeof(section));
+                *prev_name = '\0';
+            }
+            else if (!error) {
+                /* No ']' found on section line */
+                error = lineno;
+            }
+        }
+        else if (*start) {
+            /* Not a comment, must be a name[=:]value pair */
+            end = find_chars_or_comment(start, "=:");
+            if (*end == '=' || *end == ':') {
+                *end = '\0';
+                name = rstrip(start);
+                value = end + 1;
+#if INI_ALLOW_INLINE_COMMENTS
+                end = find_chars_or_comment(value, NULL);
+                if (*end)
+                    *end = '\0';
+#endif
+                value = lskip(value);
+                rstrip(value);
+
+                /* Valid name[=:]value pair found, call handler */
+                strncpy0(prev_name, name, sizeof(prev_name));
+                if (!HANDLER(user, section, name, value) && !error)
+                    error = lineno;
+            }
+            else if (!error) {
+                /* No '=' or ':' found on name[=:]value line */
+                error = lineno;
+            }
+        }
+
+#if INI_STOP_ON_FIRST_ERROR
+        if (error)
+            break;
+#endif
+    }
+
+#if !INI_USE_STACK
+    free(line);
+#endif
+
+    return error;
+}
+
+/* See documentation in header file. */
+int ini_parse_file(FILE* file, ini_handler handler, void* user)
+{
+    return ini_parse_stream((ini_reader)fgets, file, handler, user);
+}
+
+/* See documentation in header file. */
+int ini_parse(const char* filename, ini_handler handler, void* user)
+{
+    FILE* file;
+    int error;
+
+    file = fopen(filename, "r");
+    if (!file)
+        return -1;
+    error = ini_parse_file(file, handler, user);
+    fclose(file);
+    return error;
+}
diff --git a/ini.h b/ini.h
@@ -0,0 +1,104 @@
+/* inih -- simple .INI file parser
+
+inih is released under the New BSD license (see LICENSE.txt). Go to the project
+home page for more info:
+
+https://github.com/benhoyt/inih
+
+*/
+
+#ifndef __INI_H__
+#define __INI_H__
+
+/* Make this header file easier to include in C++ code */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+
+/* Nonzero if ini_handler callback should accept lineno parameter. */
+#ifndef INI_HANDLER_LINENO
+#define INI_HANDLER_LINENO 0
+#endif
+
+/* Typedef for prototype of handler function. */
+#if INI_HANDLER_LINENO
+typedef int (*ini_handler)(void* user, const char* section,
+                           const char* name, const char* value,
+                           int lineno);
+#else
+typedef int (*ini_handler)(void* user, const char* section,
+                           const char* name, const char* value);
+#endif
+
+/* Typedef for prototype of fgets-style reader function. */
+typedef char* (*ini_reader)(char* str, int num, void* stream);
+
+/* Parse given INI-style file. May have [section]s, name=value pairs
+   (whitespace stripped), and comments starting with ';' (semicolon). Section
+   is "" if name=value pair parsed before any section heading. name:value
+   pairs are also supported as a concession to Python's configparser.
+
+   For each name=value pair parsed, call handler function with given user
+   pointer as well as section, name, and value (data only valid for duration
+   of handler call). Handler should return nonzero on success, zero on error.
+
+   Returns 0 on success, line number of first error on parse error (doesn't
+   stop on first error), -1 on file open error, or -2 on memory allocation
+   error (only when INI_USE_STACK is zero).
+*/
+int ini_parse(const char* filename, ini_handler handler, void* user);
+
+/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
+   close the file when it's finished -- the caller must do that. */
+int ini_parse_file(FILE* file, ini_handler handler, void* user);
+
+/* Same as ini_parse(), but takes an ini_reader function pointer instead of
+   filename. Used for implementing custom or string-based I/O. */
+int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
+                     void* user);
+
+/* Nonzero to allow multi-line value parsing, in the style of Python's
+   configparser. If allowed, ini_parse() will call the handler with the same
+   name for each subsequent line parsed. */
+#ifndef INI_ALLOW_MULTILINE
+#define INI_ALLOW_MULTILINE 1
+#endif
+
+/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
+   the file. See http://code.google.com/p/inih/issues/detail?id=21 */
+#ifndef INI_ALLOW_BOM
+#define INI_ALLOW_BOM 1
+#endif
+
+/* Nonzero to allow inline comments (with valid inline comment characters
+   specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
+   Python 3.2+ configparser behaviour. */
+#ifndef INI_ALLOW_INLINE_COMMENTS
+#define INI_ALLOW_INLINE_COMMENTS 1
+#endif
+#ifndef INI_INLINE_COMMENT_PREFIXES
+#define INI_INLINE_COMMENT_PREFIXES ";"
+#endif
+
+/* Nonzero to use stack, zero to use heap (malloc/free). */
+#ifndef INI_USE_STACK
+#define INI_USE_STACK 1
+#endif
+
+/* Stop parsing on first error (default is to keep parsing). */
+#ifndef INI_STOP_ON_FIRST_ERROR
+#define INI_STOP_ON_FIRST_ERROR 0
+#endif
+
+/* Maximum line length for any line in INI file. */
+#ifndef INI_MAX_LINE
+#define INI_MAX_LINE 200
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __INI_H__ */
diff --git a/khash.h b/khash.h
@@ -0,0 +1,627 @@
+/* The MIT License
+
+   Copyright (c) 2008, 2009, 2011 by Attractive Chaos <attractor@live.co.uk>
+
+   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.
+*/
+
+/*
+  An example:
+
+#include "khash.h"
+KHASH_MAP_INIT_INT(32, char)
+int main() {
+	int ret, is_missing;
+	khiter_t k;
+	khash_t(32) *h = kh_init(32);
+	k = kh_put(32, h, 5, &ret);
+	kh_value(h, k) = 10;
+	k = kh_get(32, h, 10);
+	is_missing = (k == kh_end(h));
+	k = kh_get(32, h, 5);
+	kh_del(32, h, k);
+	for (k = kh_begin(h); k != kh_end(h); ++k)
+		if (kh_exist(h, k)) kh_value(h, k) = 1;
+	kh_destroy(32, h);
+	return 0;
+}
+*/
+
+/*
+  2013-05-02 (0.2.8):
+
+	* Use quadratic probing. When the capacity is power of 2, stepping function
+	  i*(i+1)/2 guarantees to traverse each bucket. It is better than double
+	  hashing on cache performance and is more robust than linear probing.
+
+	  In theory, double hashing should be more robust than quadratic probing.
+	  However, my implementation is probably not for large hash tables, because
+	  the second hash function is closely tied to the first hash function,
+	  which reduce the effectiveness of double hashing.
+
+	Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php
+
+  2011-12-29 (0.2.7):
+
+    * Minor code clean up; no actual effect.
+
+  2011-09-16 (0.2.6):
+
+	* The capacity is a power of 2. This seems to dramatically improve the
+	  speed for simple keys. Thank Zilong Tan for the suggestion. Reference:
+
+	   - http://code.google.com/p/ulib/
+	   - http://nothings.org/computer/judy/
+
+	* Allow to optionally use linear probing which usually has better
+	  performance for random input. Double hashing is still the default as it
+	  is more robust to certain non-random input.
+
+	* Added Wang's integer hash function (not used by default). This hash
+	  function is more robust to certain non-random input.
+
+  2011-02-14 (0.2.5):
+
+    * Allow to declare global functions.
+
+  2009-09-26 (0.2.4):
+
+    * Improve portability
+
+  2008-09-19 (0.2.3):
+
+	* Corrected the example
+	* Improved interfaces
+
+  2008-09-11 (0.2.2):
+
+	* Improved speed a little in kh_put()
+
+  2008-09-10 (0.2.1):
+
+	* Added kh_clear()
+	* Fixed a compiling error
+
+  2008-09-02 (0.2.0):
+
+	* Changed to token concatenation which increases flexibility.
+
+  2008-08-31 (0.1.2):
+
+	* Fixed a bug in kh_get(), which has not been tested previously.
+
+  2008-08-31 (0.1.1):
+
+	* Added destructor
+*/
+
+
+#ifndef __AC_KHASH_H
+#define __AC_KHASH_H
+
+/*!
+  @header
+
+  Generic hash table library.
+ */
+
+#define AC_VERSION_KHASH_H "0.2.8"
+
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+
+/* compiler specific configuration */
+
+#if UINT_MAX == 0xffffffffu
+typedef unsigned int khint32_t;
+#elif ULONG_MAX == 0xffffffffu
+typedef unsigned long khint32_t;
+#endif
+
+#if ULONG_MAX == ULLONG_MAX
+typedef unsigned long khint64_t;
+#else
+typedef unsigned long long khint64_t;
+#endif
+
+#ifndef kh_inline
+#ifdef _MSC_VER
+#define kh_inline __inline
+#else
+#define kh_inline inline
+#endif
+#endif /* kh_inline */
+
+#ifndef klib_unused
+#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3)
+#define klib_unused __attribute__ ((__unused__))
+#else
+#define klib_unused
+#endif
+#endif /* klib_unused */
+
+typedef khint32_t khint_t;
+typedef khint_t khiter_t;
+
+#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2)
+#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1)
+#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3)
+#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1)))
+#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1)))
+#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1)))
+#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1))
+
+#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4)
+
+#ifndef kroundup32
+#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
+#endif
+
+#ifndef kcalloc
+#define kcalloc(N,Z) calloc(N,Z)
+#endif
+#ifndef kmalloc
+#define kmalloc(Z) malloc(Z)
+#endif
+#ifndef krealloc
+#define krealloc(P,Z) realloc(P,Z)
+#endif
+#ifndef kfree
+#define kfree(P) free(P)
+#endif
+
+static const double __ac_HASH_UPPER = 0.77;
+
+#define __KHASH_TYPE(name, khkey_t, khval_t) \
+	typedef struct kh_##name##_s { \
+		khint_t n_buckets, size, n_occupied, upper_bound; \
+		khint32_t *flags; \
+		khkey_t *keys; \
+		khval_t *vals; \
+	} kh_##name##_t;
+
+#define __KHASH_PROTOTYPES(name, khkey_t, khval_t)	 					\
+	extern kh_##name##_t *kh_init_##name(void);							\
+	extern void kh_destroy_##name(kh_##name##_t *h);					\
+	extern void kh_clear_##name(kh_##name##_t *h);						\
+	extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); 	\
+	extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \
+	extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \
+	extern void kh_del_##name(kh_##name##_t *h, khint_t x);
+
+#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
+	SCOPE kh_##name##_t *kh_init_##name(void) {							\
+		return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t));		\
+	}																	\
+	SCOPE void kh_destroy_##name(kh_##name##_t *h)						\
+	{																	\
+		if (h) {														\
+			kfree((void *)h->keys); kfree(h->flags);					\
+			kfree((void *)h->vals);										\
+			kfree(h);													\
+		}																\
+	}																	\
+	SCOPE void kh_clear_##name(kh_##name##_t *h)						\
+	{																	\
+		if (h && h->flags) {											\
+			memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \
+			h->size = h->n_occupied = 0;								\
+		}																\
+	}																	\
+	SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) 	\
+	{																	\
+		if (h->n_buckets) {												\
+			khint_t k, i, last, mask, step = 0; \
+			mask = h->n_buckets - 1;									\
+			k = __hash_func(key); i = k & mask;							\
+			last = i; \
+			while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
+				i = (i + (++step)) & mask; \
+				if (i == last) return h->n_buckets;						\
+			}															\
+			return __ac_iseither(h->flags, i)? h->n_buckets : i;		\
+		} else return 0;												\
+	}																	\
+	SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \
+	{ /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \
+		khint32_t *new_flags = 0;										\
+		khint_t j = 1;													\
+		{																\
+			kroundup32(new_n_buckets); 									\
+			if (new_n_buckets < 4) new_n_buckets = 4;					\
+			if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0;	/* requested size is too small */ \
+			else { /* hash table size to be changed (shrink or expand); rehash */ \
+				new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t));	\
+				if (!new_flags) return -1;								\
+				memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
+				if (h->n_buckets < new_n_buckets) {	/* expand */		\
+					khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
+					if (!new_keys) { kfree(new_flags); return -1; }		\
+					h->keys = new_keys;									\
+					if (kh_is_map) {									\
+						khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
+						if (!new_vals) { kfree(new_flags); return -1; }	\
+						h->vals = new_vals;								\
+					}													\
+				} /* otherwise shrink */								\
+			}															\
+		}																\
+		if (j) { /* rehashing is needed */								\
+			for (j = 0; j != h->n_buckets; ++j) {						\
+				if (__ac_iseither(h->flags, j) == 0) {					\
+					khkey_t key = h->keys[j];							\
+					khval_t val;										\
+					khint_t new_mask;									\
+					new_mask = new_n_buckets - 1; 						\
+					if (kh_is_map) val = h->vals[j];					\
+					__ac_set_isdel_true(h->flags, j);					\
+					while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \
+						khint_t k, i, step = 0; \
+						k = __hash_func(key);							\
+						i = k & new_mask;								\
+						while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \
+						__ac_set_isempty_false(new_flags, i);			\
+						if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \
+							{ khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \
+							if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \
+							__ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \
+						} else { /* write the element and jump out of the loop */ \
+							h->keys[i] = key;							\
+							if (kh_is_map) h->vals[i] = val;			\
+							break;										\
+						}												\
+					}													\
+				}														\
+			}															\
+			if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \
+				h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
+				if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
+			}															\
+			kfree(h->flags); /* free the working space */				\
+			h->flags = new_flags;										\
+			h->n_buckets = new_n_buckets;								\
+			h->n_occupied = h->size;									\
+			h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \
+		}																\
+		return 0;														\
+	}																	\
+	SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \
+	{																	\
+		khint_t x;														\
+		if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \
+			if (h->n_buckets > (h->size<<1)) {							\
+				if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \
+					*ret = -1; return h->n_buckets;						\
+				}														\
+			} else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \
+				*ret = -1; return h->n_buckets;							\
+			}															\
+		} /* TODO: to implement automatically shrinking; resize() already support shrinking */ \
+		{																\
+			khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \
+			x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \
+			if (__ac_isempty(h->flags, i)) x = i; /* for speed up */	\
+			else {														\
+				last = i; \
+				while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
+					if (__ac_isdel(h->flags, i)) site = i;				\
+					i = (i + (++step)) & mask; \
+					if (i == last) { x = site; break; }					\
+				}														\
+				if (x == h->n_buckets) {								\
+					if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \
+					else x = i;											\
+				}														\
+			}															\
+		}																\
+		if (__ac_isempty(h->flags, x)) { /* not present at all */		\
+			h->keys[x] = key;											\
+			__ac_set_isboth_false(h->flags, x);							\
+			++h->size; ++h->n_occupied;									\
+			*ret = 1;													\
+		} else if (__ac_isdel(h->flags, x)) { /* deleted */				\
+			h->keys[x] = key;											\
+			__ac_set_isboth_false(h->flags, x);							\
+			++h->size;													\
+			*ret = 2;													\
+		} else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \
+		return x;														\
+	}																	\
+	SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x)				\
+	{																	\
+		if (x != h->n_buckets && !__ac_iseither(h->flags, x)) {			\
+			__ac_set_isdel_true(h->flags, x);							\
+			--h->size;													\
+		}																\
+	}
+
+#define KHASH_DECLARE(name, khkey_t, khval_t)		 					\
+	__KHASH_TYPE(name, khkey_t, khval_t) 								\
+	__KHASH_PROTOTYPES(name, khkey_t, khval_t)
+
+#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
+	__KHASH_TYPE(name, khkey_t, khval_t) 								\
+	__KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
+
+#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
+	KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
+
+/* --- BEGIN OF HASH FUNCTIONS --- */
+
+/*! @function
+  @abstract     Integer hash function
+  @param  key   The integer [khint32_t]
+  @return       The hash value [khint_t]
+ */
+#define kh_int_hash_func(key) (khint32_t)(key)
+/*! @function
+  @abstract     Integer comparison function
+ */
+#define kh_int_hash_equal(a, b) ((a) == (b))
+/*! @function
+  @abstract     64-bit integer hash function
+  @param  key   The integer [khint64_t]
+  @return       The hash value [khint_t]
+ */
+#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11)
+/*! @function
+  @abstract     64-bit integer comparison function
+ */
+#define kh_int64_hash_equal(a, b) ((a) == (b))
+/*! @function
+  @abstract     const char* hash function
+  @param  s     Pointer to a null terminated string
+  @return       The hash value
+ */
+static kh_inline khint_t __ac_X31_hash_string(const char *s)
+{
+	khint_t h = (khint_t)*s;
+	if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s;
+	return h;
+}
+/*! @function
+  @abstract     Another interface to const char* hash function
+  @param  key   Pointer to a null terminated string [const char*]
+  @return       The hash value [khint_t]
+ */
+#define kh_str_hash_func(key) __ac_X31_hash_string(key)
+/*! @function
+  @abstract     Const char* comparison function
+ */
+#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0)
+
+static kh_inline khint_t __ac_Wang_hash(khint_t key)
+{
+    key += ~(key << 15);
+    key ^=  (key >> 10);
+    key +=  (key << 3);
+    key ^=  (key >> 6);
+    key += ~(key << 11);
+    key ^=  (key >> 16);
+    return key;
+}
+#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key)
+
+/* --- END OF HASH FUNCTIONS --- */
+
+/* Other convenient macros... */
+
+/*!
+  @abstract Type of the hash table.
+  @param  name  Name of the hash table [symbol]
+ */
+#define khash_t(name) kh_##name##_t
+
+/*! @function
+  @abstract     Initiate a hash table.
+  @param  name  Name of the hash table [symbol]
+  @return       Pointer to the hash table [khash_t(name)*]
+ */
+#define kh_init(name) kh_init_##name()
+
+/*! @function
+  @abstract     Destroy a hash table.
+  @param  name  Name of the hash table [symbol]
+  @param  h     Pointer to the hash table [khash_t(name)*]
+ */
+#define kh_destroy(name, h) kh_destroy_##name(h)
+
+/*! @function
+  @abstract     Reset a hash table without deallocating memory.
+  @param  name  Name of the hash table [symbol]
+  @param  h     Pointer to the hash table [khash_t(name)*]
+ */
+#define kh_clear(name, h) kh_clear_##name(h)
+
+/*! @function
+  @abstract     Resize a hash table.
+  @param  name  Name of the hash table [symbol]
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @param  s     New size [khint_t]
+ */
+#define kh_resize(name, h, s) kh_resize_##name(h, s)
+
+/*! @function
+  @abstract     Insert a key to the hash table.
+  @param  name  Name of the hash table [symbol]
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @param  k     Key [type of keys]
+  @param  r     Extra return code: -1 if the operation failed;
+                0 if the key is present in the hash table;
+                1 if the bucket is empty (never used); 2 if the element in
+				the bucket has been deleted [int*]
+  @return       Iterator to the inserted element [khint_t]
+ */
+#define kh_put(name, h, k, r) kh_put_##name(h, k, r)
+
+/*! @function
+  @abstract     Retrieve a key from the hash table.
+  @param  name  Name of the hash table [symbol]
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @param  k     Key [type of keys]
+  @return       Iterator to the found element, or kh_end(h) if the element is absent [khint_t]
+ */
+#define kh_get(name, h, k) kh_get_##name(h, k)
+
+/*! @function
+  @abstract     Remove a key from the hash table.
+  @param  name  Name of the hash table [symbol]
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @param  k     Iterator to the element to be deleted [khint_t]
+ */
+#define kh_del(name, h, k) kh_del_##name(h, k)
+
+/*! @function
+  @abstract     Test whether a bucket contains data.
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @param  x     Iterator to the bucket [khint_t]
+  @return       1 if containing data; 0 otherwise [int]
+ */
+#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x)))
+
+/*! @function
+  @abstract     Get key given an iterator
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @param  x     Iterator to the bucket [khint_t]
+  @return       Key [type of keys]
+ */
+#define kh_key(h, x) ((h)->keys[x])
+
+/*! @function
+  @abstract     Get value given an iterator
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @param  x     Iterator to the bucket [khint_t]
+  @return       Value [type of values]
+  @discussion   For hash sets, calling this results in segfault.
+ */
+#define kh_val(h, x) ((h)->vals[x])
+
+/*! @function
+  @abstract     Alias of kh_val()
+ */
+#define kh_value(h, x) ((h)->vals[x])
+
+/*! @function
+  @abstract     Get the start iterator
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @return       The start iterator [khint_t]
+ */
+#define kh_begin(h) (khint_t)(0)
+
+/*! @function
+  @abstract     Get the end iterator
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @return       The end iterator [khint_t]
+ */
+#define kh_end(h) ((h)->n_buckets)
+
+/*! @function
+  @abstract     Get the number of elements in the hash table
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @return       Number of elements in the hash table [khint_t]
+ */
+#define kh_size(h) ((h)->size)
+
+/*! @function
+  @abstract     Get the number of buckets in the hash table
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @return       Number of buckets in the hash table [khint_t]
+ */
+#define kh_n_buckets(h) ((h)->n_buckets)
+
+/*! @function
+  @abstract     Iterate over the entries in the hash table
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @param  kvar  Variable to which key will be assigned
+  @param  vvar  Variable to which value will be assigned
+  @param  code  Block of code to execute
+ */
+#define kh_foreach(h, kvar, vvar, code) { khint_t __i;		\
+	for (__i = kh_begin(h); __i != kh_end(h); ++__i) {		\
+		if (!kh_exist(h,__i)) continue;						\
+		(kvar) = kh_key(h,__i);								\
+		(vvar) = kh_val(h,__i);								\
+		code;												\
+	} }
+
+/*! @function
+  @abstract     Iterate over the values in the hash table
+  @param  h     Pointer to the hash table [khash_t(name)*]
+  @param  vvar  Variable to which value will be assigned
+  @param  code  Block of code to execute
+ */
+#define kh_foreach_value(h, vvar, code) { khint_t __i;		\
+	for (__i = kh_begin(h); __i != kh_end(h); ++__i) {		\
+		if (!kh_exist(h,__i)) continue;						\
+		(vvar) = kh_val(h,__i);								\
+		code;												\
+	} }
+
+/* More conenient interfaces */
+
+/*! @function
+  @abstract     Instantiate a hash set containing integer keys
+  @param  name  Name of the hash table [symbol]
+ */
+#define KHASH_SET_INIT_INT(name)										\
+	KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal)
+
+/*! @function
+  @abstract     Instantiate a hash map containing integer keys
+  @param  name  Name of the hash table [symbol]
+  @param  khval_t  Type of values [type]
+ */
+#define KHASH_MAP_INIT_INT(name, khval_t)								\
+	KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal)
+
+/*! @function
+  @abstract     Instantiate a hash map containing 64-bit integer keys
+  @param  name  Name of the hash table [symbol]
+ */
+#define KHASH_SET_INIT_INT64(name)										\
+	KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal)
+
+/*! @function
+  @abstract     Instantiate a hash map containing 64-bit integer keys
+  @param  name  Name of the hash table [symbol]
+  @param  khval_t  Type of values [type]
+ */
+#define KHASH_MAP_INIT_INT64(name, khval_t)								\
+	KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal)
+
+typedef const char *kh_cstr_t;
+/*! @function
+  @abstract     Instantiate a hash map containing const char* keys
+  @param  name  Name of the hash table [symbol]
+ */
+#define KHASH_SET_INIT_STR(name)										\
+	KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal)
+
+/*! @function
+  @abstract     Instantiate a hash map containing const char* keys
+  @param  name  Name of the hash table [symbol]
+  @param  khval_t  Type of values [type]
+ */
+#define KHASH_MAP_INIT_STR(name, khval_t)								\
+	KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal)
+
+#endif /* __AC_KHASH_H */
diff --git a/ltk.c b/ltk.c
@@ -0,0 +1,563 @@
+/*
+ * 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 <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 "khash.h"
+#include "ini.h"
+#include "ltk.h"
+#include "grid.h"
+#include "button.h"
+
+static void ltk_load_theme(ltk_window *window, const char *path);
+static void ltk_destroy_theme(ltk_theme *theme);
+
+static int running = 1;
+
+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) {
+	XCloseDisplay(window->dpy);
+	ltk_destroy_theme(window->theme);
+	ltk_destroy_window(window);
+}
+
+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);
+};
+
+XColor
+ltk_create_xcolor(ltk_window *window, const char *hex) {
+	XColor color;
+	XParseColor(window->dpy, window->cm, hex, &color);
+	XAllocColor(window->dpy, window->cm, &color);
+
+	return color;
+}
+
+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
+create_widget(ltk_window *window, char tokens[10][20], size_t num_tokens) {
+	if (num_tokens < 3) {
+		(void)fprintf(stderr, "Invalid number of arguments.\n");
+		return;
+	}
+	ltk_widget *widget;
+	khint_t k = kh_get(widget, window->widget_hash, tokens[2]);
+	if (k != kh_end(window->widget_hash)) {
+		(void)fprintf(stderr, "Widget id already exists.\n");
+		return;
+	}
+	if (strcmp(tokens[1], "button") == 0) {
+		/* yeah, text is currently ignored... */
+		widget = ltk_button_create(window, tokens[2], "I'm a button!");
+	} else {
+		(void)fprintf(stderr, "Invalid widget type.\n");
+		return;
+	}
+	int ret;
+	/* apparently, khash requires the string to stay accessible */
+	/* FIXME: actually free this hash table in the end... */
+	char *tmp = strdup(tokens[2]);
+	k = kh_put(widget, window->widget_hash, tmp, &ret);
+	kh_value(window->widget_hash, k) = widget;
+}
+
+static void
+grid_widget(ltk_window *window, char tokens[10][20], size_t num_tokens) {
+	if (num_tokens < 4) {
+		(void)fprintf(stderr, "Invalid number of arguments.\n");
+		return;
+	}
+	ltk_widget *widget;
+	khint_t k = kh_get(widget, window->widget_hash, tokens[1]);
+	if (k == kh_end(window->widget_hash)) {
+		(void)fprintf(stderr, "Widget with given id doesn't exist.\n");
+		return;
+	}
+	widget = kh_value(window->widget_hash, k);
+	/* FIXME: error checking */
+	int row = atoi(tokens[2]);
+	int column = atoi(tokens[3]);
+	ltk_grid_widget(widget, window->root_widget, row, column, 1, 1, LTK_STICKY_LEFT | LTK_STICKY_RIGHT);
+	ltk_window_invalidate_rect(window, window->root_widget->rect);
+}
+
+/* copied from suckless ii */
+static int
+read_line(int fd, char *buf, size_t bufsiz)
+{
+	size_t i = 0;
+	char c = '\0';
+	do {
+		if (read(fd, &c, sizeof(char)) != sizeof(char))
+			return -1;
+		buf[i++] = c;
+	} while (c != '\n' && i < bufsiz);
+	buf[i - 1] = '\0'; /* eliminates '\n' */
+	return 0;
+}
+
+static size_t
+tokenize(char tokens[10][20], char *buf) {
+	char *c;
+	if (!buf || buf[0] == '\0') return 0;
+	for (c = buf; *c == ' '; c++)
+		;
+	size_t cur_tok = 0;
+	size_t cur_tok_len = 0;
+	while (*c != '\0') {
+		if (cur_tok >= 10) {
+			return 0;
+		} else if (*c == ' ') {
+			tokens[cur_tok][cur_tok_len] = '\0';
+			cur_tok++;
+			cur_tok_len = 0;
+		} else if (cur_tok_len >= 19) {
+			return 0;
+		} else {
+			tokens[cur_tok][cur_tok_len++] = *c;
+		}
+		c++;
+	}
+	tokens[cur_tok][cur_tok_len] = '\0';
+	return cur_tok + 1;
+}
+
+static void
+proc_cmds(ltk_window *window, char *buf) {
+	char tokens[10][20];
+	int num_tokens;
+	if (!(num_tokens = tokenize(tokens, buf))) return;
+	if (strcmp(tokens[0], "create") == 0) {
+		create_widget(window, tokens, num_tokens);
+	} else if (strcmp(tokens[0], "grid") == 0) {
+		grid_widget(window, tokens, num_tokens);
+	} else {
+		(void)fprintf(stderr, "Invalid command.\n");
+	}
+}
+
+/* copied from suckless st */
+#define TIMEDIFF(t1, t2)        ((t1.tv_sec-t2.tv_sec)*1000 + \
+                                (t1.tv_nsec-t2.tv_nsec)/1E6)
+
+/* FIXME: destroy remaining widgets in hash on exit */
+int
+ltk_mainloop(ltk_window *window) {
+	XEvent event;
+	int fd_in;
+        struct stat st;
+        struct timespec tick;
+	tick.tv_sec = 0;
+	tick.tv_nsec = 10000;
+	char buf[200]; /* FIXME: what would be sensible? */
+
+        if (lstat("ltk_in", &st) != -1) {
+		/* FIXME... */
+		unlink("ltk_in");
+	}
+
+	if (mkfifo("ltk_in", S_IRWXU)) return -1;
+	fd_in = open("ltk_in", O_RDONLY | O_NONBLOCK, 0);
+	if (fd_in == -1) return -1;
+	while (running) {
+		while (XPending(window->dpy)) {
+			XNextEvent(window->dpy, &event);
+			ltk_handle_event(window, event);
+		}
+		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;
+		} else if (window->last_event) {
+			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;
+		} else if (!read_line(fd_in, buf, sizeof(buf))) {
+			proc_cmds(window, buf);
+		}
+		/* yes, this should be improved */
+		nanosleep(&tick, NULL);
+
+	}
+	unlink("ltk_in");
+}
+
+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);
+	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;
+
+	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) {
+	ltk_widget *ptr = window->root_widget;
+	if (ptr) ptr->destroy(ptr);
+	XDestroyWindow(window->dpy, window->xwindow);
+	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) {
+	if (strcmp(prop, "border_width") == 0) {
+		window->theme->window->border_width = atoi(value);
+	} else if (strcmp(prop, "bg") == 0) {
+		window->theme->window->bg = ltk_create_xcolor(window, value);
+	} else if (strcmp(prop, "fg") == 0) {
+		window->theme->window->fg = ltk_create_xcolor(window, value);
+	} else if (strcmp(prop, "font") == 0) {
+		window->theme->window->font = strdup(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->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 (*destroy) (void *), unsigned int needs_redraw) {
+	widget->id = strdup(id);
+	widget->window = window;
+	widget->active_widget = NULL;
+	widget->parent = NULL;
+
+	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->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->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->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->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 main(int argc, char *argv[]) {
+	ltk_window *window = ltk_create_window("theme.ini", "Demo", 0, 0, 500, 500);
+	ltk_grid *grid = ltk_create_grid(window, "grid", 2, 2);
+	window->root_widget = grid;
+	ltk_set_row_weight(grid, 0, 1);
+	ltk_set_row_weight(grid, 1, 1);
+	ltk_set_column_weight(grid, 0, 1);
+	ltk_set_column_weight(grid, 1, 1);
+	ltk_mainloop(window);
+}
diff --git a/ltk.h b/ltk.h
@@ -0,0 +1,158 @@
+/*
+ * This file is part of the Lumidify ToolKit (LTK)
+ * Copyright (c) 2016, 2017, 2018 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_H_
+#define _LTK_H_
+
+/* Requires the following includes: <X11/Xlib.h>, <X11/Xutil.h>, "drw.h" */
+
+typedef struct {
+	int x;
+	int y;
+	int w;
+	int h;
+} ltk_rect;
+
+typedef enum {
+	LTK_STICKY_LEFT = 1 << 0,
+	LTK_STICKY_RIGHT = 1 << 1,
+	LTK_STICKY_TOP = 1 << 2,
+	LTK_STICKY_BOTTOM = 1 << 3
+} ltk_sticky_mask;
+
+typedef enum {
+	LTK_NORMAL,
+	LTK_PRESSED,
+	LTK_ACTIVE,
+	LTK_DISABLED
+} ltk_widget_state;
+
+typedef struct ltk_window ltk_window;
+
+typedef struct ltk_widget {
+	ltk_window *window;
+	struct ltk_widget *active_widget;
+	struct ltk_widget *parent;
+	char *id;
+
+	void (*key_press) (void *, XEvent event);
+	void (*key_release) (void *, XEvent event);
+	void (*mouse_press) (void *, XEvent event);
+	void (*mouse_release) (void *, XEvent event);
+	void (*motion_notify) (void *, XEvent event);
+	void (*mouse_leave) (void *, XEvent event);
+	void (*mouse_enter) (void *, XEvent event);
+
+	void (*resize) (void *, int, int);
+	void (*draw) (void *);
+	void (*destroy) (void *);
+
+	ltk_rect rect;
+	unsigned int row;
+	unsigned int column;
+	unsigned int row_span;
+	unsigned int column_span;
+	unsigned int needs_redraw;
+	ltk_widget_state state;
+	unsigned short sticky;
+} ltk_widget;
+
+typedef struct {
+	int border_width;
+	char *font;
+	XColor fg;
+	XColor bg;
+} ltk_window_theme;
+
+typedef struct ltk_button_theme ltk_button_theme;
+
+typedef struct {
+	ltk_window_theme *window;
+	ltk_button_theme *button;
+} ltk_theme;
+
+struct ltk_event_queue {
+	char *id;
+	char *name;
+	struct ltk_event_queue *prev;
+	struct ltk_event_queue *next;
+};
+
+KHASH_MAP_INIT_STR(widget, ltk_widget *)
+
+typedef struct ltk_window {
+	Display *dpy;
+	Colormap cm;
+	GC gc;
+	int screen;
+	Atom wm_delete_msg;
+	Window xwindow;
+	ltk_widget *root_widget;
+	ltk_widget *active_widget;
+	int (*other_event) (ltk_window *, XEvent event);
+	ltk_rect rect;
+	ltk_theme *theme;
+	ltk_rect dirty_rect;
+	struct ltk_event_queue *first_event;
+	struct ltk_event_queue *last_event;
+	khash_t(widget) *widget_hash;
+} ltk_window;
+
+void ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect);
+
+void ltk_fatal(const char *msg);
+
+XColor ltk_create_xcolor(ltk_window *window, const char *hex);
+
+void ltk_queue_event(ltk_window *window, const char *id, const char *name);
+
+int ltk_mainloop(ltk_window *window);
+
+ltk_window *ltk_create_window(
+    const char *theme_path, const char *title,
+    int x, int y, unsigned int w, unsigned int h);
+
+void ltk_redraw_window(ltk_window *window);
+
+void ltk_destroy_window(ltk_window *window);
+
+void ltk_window_other_event(ltk_window *window, XEvent event);
+
+int ltk_collide_rect(ltk_rect rect, int x, int y);
+
+void ltk_remove_active_widget(ltk_widget *widget);
+
+void ltk_set_active_widget(ltk_window *window, ltk_widget *widget);
+
+void ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window * window,
+	void (*draw) (void *), void (*destroy) (void *), unsigned int needs_redraw);
+
+void ltk_widget_mouse_press_event(ltk_widget *widget, XEvent event);
+
+void ltk_widget_mouse_release_event(ltk_widget *widget, XEvent event);
+
+void ltk_widget_motion_notify_event(ltk_widget *widget, XEvent event);
+
+void ltk_handle_event(ltk_window *window, XEvent event);
+
+#endif
diff --git a/test.sh b/test.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+echo "create button btn1" > ltk_in
+echo "grid btn1 0 0" > ltk_in
+echo "create button btn2" > ltk_in
+echo "grid btn2 1 1" > ltk_in
diff --git a/theme.ini b/theme.ini
@@ -0,0 +1,18 @@
+[window]
+border_width = 0
+bg = #000000
+fg = #FFFFFF
+font = Awami Nastaliq
+
+[button]
+border_width = 2
+text_color = #FFFFFF
+pad = 5
+border = #339999
+fill = #113355
+border_pressed = #FFFFFF
+fill_pressed = #113355
+border_active = #FFFFFF
+fill_active = #738194
+border_disabled = #FFFFFF
+fill_disabled = #292929