URI: 
       tAdd basic image support; add more options to grid - ltk - Socket-based GUI for X11 (WIP)
  HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 462218a997dc7c37f972e1a0bcf73d96548f8999
   DIR parent 25a158327d3d12d3eba052f202278ffa0d5539d5
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Wed, 15 Nov 2023 22:51:31 +0100
       
       Add basic image support; add more options to grid
       
       Diffstat:
         M Makefile                            |      17 ++++++++++++-----
         M README.md                           |       3 +++
         M src/.gitignore                      |       1 +
         M src/box.c                           |      25 ++++++++++++++-----------
         M src/box.h                           |       3 ++-
         M src/button.c                        |       4 ++--
         M src/button.h                        |       8 ++------
         M src/cmd.c                           |      49 +++++++++++++++++++++----------
         M src/cmd.h                           |      31 +++++++++++++++++++++++++------
         M src/config.c                        |       3 ++-
         M src/entry.c                         |       5 +++--
         M src/entry.h                         |       8 ++------
         M src/err.c                           |       1 +
         M src/err.h                           |       9 +++++----
         M src/grid.c                          |      82 +++++++++++++++++++++----------
         M src/grid.h                          |       9 +++++----
         A src/image.c                         |     112 +++++++++++++++++++++++++++++++
         A src/image.h                         |      48 +++++++++++++++++++++++++++++++
         A src/image_widget.c                  |     154 +++++++++++++++++++++++++++++++
         A src/image_widget.h                  |      31 +++++++++++++++++++++++++++++++
         M src/label.c                         |       5 +++--
         M src/label.h                         |       8 ++------
         M src/ltk.h                           |       9 ++++++++-
         M src/ltkc.c                          |     199 ++++++++++++++++++-------------
         A src/ltkc_img.c                      |      26 ++++++++++++++++++++++++++
         M src/ltkd.c                          |     273 +++++++++++++++++++++----------
         M src/macros.h                        |       2 ++
         M src/menu.c                          |      22 +++++++++++-----------
         M src/menu.h                          |      16 +++-------------
         M src/proto_types.h                   |      22 ++++++++++++----------
         M src/text_pango.c                    |       1 +
         M src/util.c                          |      11 +++++++++++
         M src/util.h                          |       2 ++
         M src/widget.c                        |      17 +++++++++++++----
         M src/widget.h                        |       9 ++++++---
         M test.gui                            |      16 ++++++++--------
         M test2.gui                           |       2 +-
         M test3.gui                           |       2 +-
         M test3.sh                            |       2 +-
         A testimg.sh                          |      13 +++++++++++++
       
       40 files changed, 941 insertions(+), 319 deletions(-)
       ---
   DIR diff --git a/Makefile b/Makefile
       t@@ -35,8 +35,8 @@ EXTRA_OBJ = $(EXTRA_OBJ_$(USE_PANGO))
        EXTRA_CFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_CFLAGS_$(DEV)) $(EXTRA_CFLAGS_$(USE_PANGO))
        EXTRA_LDFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFLAGS_$(USE_PANGO))
        
       -LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -std=c99 `pkg-config --cflags x11 fontconfig xext xcursor` -D_POSIX_C_SOURCE=200809L
       -LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext xcursor`
       +LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -std=c99 `pkg-config --cflags x11 fontconfig xext xcursor imlib2` -D_POSIX_C_SOURCE=200809L
       +LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext xcursor imlib2`
        
        OBJ = \
                src/strtonum.o \
       t@@ -64,6 +64,8 @@ OBJ = \
                src/txtbuf.o \
                src/ctrlsel.o \
                src/cmd.o \
       +        src/image.o \
       +        src/image_widget.o \
                $(EXTRA_OBJ)
        
        # Note: This could be improved so a change in a header only causes the .c files
       t@@ -103,16 +105,21 @@ HDR = \
                src/clipboard.h \
                src/txtbuf.h \
                src/ctrlsel.h \
       -        src/cmd.h
       +        src/cmd.h \
       +        src/image.h \
       +        src/image_widget.h
        
       -all: src/ltkd src/ltkc
       +all: src/ltkd src/ltkc src/ltkc_img
        
        src/ltkd: $(OBJ)
                $(CC) -o $@ $(OBJ) $(LTK_LDFLAGS)
        
       -src/ltkc: src/ltkc.o src/util.o src/memory.o
       +src/ltkc: src/ltkc.o src/util.o src/memory.o src/txtbuf.o
                $(CC) -o $@ src/ltkc.o src/util.o src/memory.o src/txtbuf.o $(LTK_LDFLAGS)
        
       +src/ltkc_img: src/ltkc_img.o
       +        $(CC) -o $@ src/ltkc_img.o $(LTK_LDFLAGS)
       +
        $(OBJ) : $(HDR)
        
        .c.o:
   DIR diff --git a/README.md b/README.md
       t@@ -23,3 +23,6 @@ Note: I know the default theme is butt-ugly at the moment. It is mainly
        to test things, not to look pretty.
        
        Note: Read 'socket_format.txt' for some documentation and open problems.
       +
       +Note: The image support currently requires at least Imlib2 version 1.10.0.
       +I might add compatibility support for older versions at some point.
   DIR diff --git a/src/.gitignore b/src/.gitignore
       t@@ -1,4 +1,5 @@
        ltkd
        ltkc
       +ltkc_img
        *.o
        *.core
   DIR diff --git a/src/box.c b/src/box.c
       t@@ -14,6 +14,8 @@
         * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
         */
        
       +/* FIXME: implement other sticky options now supported by grid */
       +
        #include <stdio.h>
        #include <stdlib.h>
        #include <stdarg.h>
       t@@ -36,8 +38,7 @@ static ltk_box *ltk_box_create(ltk_window *window, const char *id, ltk_orientati
        static void ltk_box_destroy(ltk_widget *self, int shallow);
        static void ltk_recalculate_box(ltk_widget *self);
        static void ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget);
       -/* FIXME: Why is sticky unsigned short? */
       -static int ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, unsigned short sticky, ltk_error *err);
       +static int ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, ltk_sticky_mask sticky, ltk_error *err);
        static int ltk_box_remove(ltk_widget *widget, ltk_widget *self, ltk_error *err);
        /* static int ltk_box_clear(ltk_window *window, ltk_box *box, int shallow, ltk_error *err); */
        static void ltk_box_scroll(ltk_widget *self);
       t@@ -90,26 +91,26 @@ static struct ltk_widget_vtable vtable = {
        static int ltk_box_cmd_add(
            ltk_window *window,
            ltk_box *box,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err);
        static int ltk_box_cmd_remove(
            ltk_window *window,
            ltk_box *box,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err);
        /*
        static int ltk_box_cmd_clear
            ltk_window *window,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err);
        */
        static int ltk_box_cmd_create(
            ltk_window *window,
            ltk_box *box,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err);
        
       t@@ -117,6 +118,7 @@ static void
        ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
                ltk_box *box = (ltk_box *)self;
                ltk_widget *ptr;
       +        /* FIXME: clip out scrollbar */
                ltk_rect real_clip = ltk_rect_intersect((ltk_rect){0, 0, self->lrect.w, self->lrect.h}, clip);
                for (size_t i = 0; i < box->num_widgets; i++) {
                        ptr = box->widgets[i];
       t@@ -195,6 +197,7 @@ ltk_box_destroy(ltk_widget *self, int shallow) {
        /* FIXME: The widget positions are set with the old scrollbar->cur_pos, before the
           virtual_size is set - this can cause problems when a widget changes its size
           (in the scrolled direction) when resized. */
       +/* FIXME: avoid complete recalculation when just scrolling (only position updated) */
        static void
        ltk_recalculate_box(ltk_widget *self) {
                ltk_box *box = (ltk_box *)self;
       t@@ -293,7 +296,7 @@ ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget) {
        }
        
        static int
       -ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, unsigned short sticky, ltk_error *err) {
       +ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, ltk_sticky_mask sticky, ltk_error *err) {
                if (widget->parent) {
                        err->type = ERR_WIDGET_IN_CONTAINER;
                        return 1;
       t@@ -495,7 +498,7 @@ static int
        ltk_box_cmd_add(
            ltk_window *window,
            ltk_box *box,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -516,7 +519,7 @@ static int
        ltk_box_cmd_remove(
            ltk_window *window,
            ltk_box *box,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -536,7 +539,7 @@ static int
        ltk_box_cmd_create(
            ltk_window *window,
            ltk_box *box_unneeded,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                (void)box_unneeded;
       t@@ -561,7 +564,7 @@ ltk_box_cmd_create(
        
        static struct box_cmd {
                char *name;
       -        int (*func)(ltk_window *, ltk_box *, char **, size_t, ltk_error *);
       +        int (*func)(ltk_window *, ltk_box *, ltk_cmd_token *, size_t, ltk_error *);
                int needs_all;
        } box_cmds[] = {
                {"add", &ltk_box_cmd_add, 0},
   DIR diff --git a/src/box.h b/src/box.h
       t@@ -20,6 +20,7 @@
        /* FIXME: include everything here */
        /* Requires the following includes: "scrollbar.h", "rect.h", "widget.h", "ltk.h" */
        
       +#include "cmd.h"
        #include "err.h"
        
        typedef struct {
       t@@ -33,6 +34,6 @@ typedef struct {
                ltk_orientation orient;
        } ltk_box;
        
       -int ltk_box_cmd(ltk_window *window, char **tokens, size_t num_tokens, ltk_error *);
       +GEN_CMD_HELPERS_PROTO(ltk_box_cmd)
        
        #endif /* _LTK_BOX_H_ */
   DIR diff --git a/src/button.c b/src/button.c
       t@@ -211,7 +211,7 @@ static int
        ltk_button_cmd_create(
            ltk_window *window,
            ltk_button *button_unneeded,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                (void)button_unneeded;
       t@@ -236,7 +236,7 @@ ltk_button_cmd_create(
        
        static struct button_cmd {
                char *name;
       -        int (*func)(ltk_window *, ltk_button *, char **, size_t, ltk_error *);
       +        int (*func)(ltk_window *, ltk_button *, ltk_cmd_token *, size_t, ltk_error *);
                int needs_all;
        } button_cmds[] = {
                {"create", &ltk_button_cmd_create, 1},
   DIR diff --git a/src/button.h b/src/button.h
       t@@ -19,6 +19,7 @@
        
        /* Requires the following includes: <X11/Xlib.h>, "rect.h", "widget.h", "ltk.h", "color.h", "text.h" */
        
       +#include "cmd.h"
        #include "err.h"
        
        typedef struct {
       t@@ -31,11 +32,6 @@ int ltk_button_ini_handler(ltk_window *window, const char *prop, const char *val
        int ltk_button_fill_theme_defaults(ltk_window *window);
        void ltk_button_uninitialize_theme(ltk_window *window);
        
       -int ltk_button_cmd(
       -    ltk_window *window,
       -    char **tokens,
       -    size_t num_tokens,
       -    ltk_error *
       -);
       +GEN_CMD_HELPERS_PROTO(ltk_button_cmd)
        
        #endif /* LTK_BUTTON_H */
   DIR diff --git a/src/cmd.c b/src/cmd.c
       t@@ -6,7 +6,7 @@
        
        int
        ltk_parse_cmd(
       -    ltk_window *window, char **tokens, size_t num_tokens,
       +    ltk_window *window, ltk_cmd_token *tokens, size_t num_tokens,
            ltk_cmdarg_parseinfo *parseinfo, size_t num_arg, ltk_error *err) {
                const char *errstr = NULL;
                if (num_tokens > num_arg || (num_tokens < num_arg && !parseinfo[num_tokens].optional)) {
       t@@ -16,9 +16,14 @@ ltk_parse_cmd(
                }
                size_t i = 0;
                for (; i < num_tokens; i++) {
       +                if (parseinfo[i].type != CMDARG_DATA && tokens[i].contains_nul) {
       +                        err->type = ERR_INVALID_ARGUMENT;
       +                        err->arg = i;
       +                        goto error;
       +                }
                        switch (parseinfo[i].type) {
                        case CMDARG_INT:
       -                        parseinfo[i].val.i = ltk_strtonum(tokens[i], parseinfo[i].min, parseinfo[i].max, &errstr);
       +                        parseinfo[i].val.i = ltk_strtonum(tokens[i].text, parseinfo[i].min, parseinfo[i].max, &errstr);
                                if (errstr) {
                                        err->type = ERR_INVALID_ARGUMENT;
                                        err->arg = i;
       t@@ -28,20 +33,29 @@ ltk_parse_cmd(
                                break;
                        case CMDARG_STICKY:
                                parseinfo[i].val.sticky = LTK_STICKY_NONE;
       -                        for (const char *c = tokens[i]; *c != '\0'; c++) {
       +                        for (const char *c = tokens[i].text; *c != '\0'; c++) {
                                        switch (*c) {
       -                                case 'n':
       +                                case 't':
                                                parseinfo[i].val.sticky |= LTK_STICKY_TOP;
                                                break;
       -                                case 's':
       +                                case 'b':
                                                parseinfo[i].val.sticky |= LTK_STICKY_BOTTOM;
                                                break;
       -                                case 'e':
       +                                case 'r':
                                                parseinfo[i].val.sticky |= LTK_STICKY_RIGHT;
                                                break;
       -                                case 'w':
       +                                case 'l':
                                                parseinfo[i].val.sticky |= LTK_STICKY_LEFT;
                                                break;
       +                                case 'w':
       +                                        parseinfo[i].val.sticky |= LTK_STICKY_SHRINK_WIDTH;
       +                                        break;
       +                                case 'h':
       +                                        parseinfo[i].val.sticky |= LTK_STICKY_SHRINK_HEIGHT;
       +                                        break;
       +                                case 'p':
       +                                        parseinfo[i].val.sticky |= LTK_STICKY_PRESERVE_ASPECT_RATIO;
       +                                        break;
                                        default:
                                                err->type = ERR_INVALID_ARGUMENT;
                                                err->arg = i;
       t@@ -51,7 +65,7 @@ ltk_parse_cmd(
                                parseinfo[i].initialized = 1;
                                break;
                        case CMDARG_WIDGET:
       -                        parseinfo[i].val.widget = ltk_get_widget(tokens[i], parseinfo[i].widget_type, err);
       +                        parseinfo[i].val.widget = ltk_get_widget(tokens[i].text, parseinfo[i].widget_type, err);
                                if (!parseinfo[i].val.widget) {
                                        err->arg = i;
                                        goto error;
       t@@ -59,11 +73,16 @@ ltk_parse_cmd(
                                parseinfo[i].initialized = 1;
                                break;
                        case CMDARG_STRING:
       -                        parseinfo[i].val.str = tokens[i];
       +                        parseinfo[i].val.str = tokens[i].text;
       +                        parseinfo[i].initialized = 1;
       +                        break;
       +                case CMDARG_DATA:
       +                        parseinfo[i].val.data = tokens[i].text;
                                parseinfo[i].initialized = 1;
       +                        parseinfo[i].len = tokens[i].len;
                                break;
                        case CMDARG_COLOR:
       -                        if (ltk_color_create(window->renderdata, tokens[i], &parseinfo[i].val.color)) {
       +                        if (ltk_color_create(window->renderdata, tokens[i].text, &parseinfo[i].val.color)) {
                                        /* FIXME: this could fail even if the argument is fine */
                                        err->type = ERR_INVALID_ARGUMENT;
                                        err->arg = i;
       t@@ -72,9 +91,9 @@ ltk_parse_cmd(
                                parseinfo[i].initialized = 1;
                                break;
                        case CMDARG_BOOL:
       -                        if (strcmp(tokens[i], "true") == 0) {
       +                        if (strcmp(tokens[i].text, "true") == 0) {
                                        parseinfo[i].val.b = 1;
       -                        } else if (strcmp(tokens[i], "false") == 0) {
       +                        } else if (strcmp(tokens[i].text, "false") == 0) {
                                        parseinfo[i].val.b = 0;
                                } else {
                                        err->type = ERR_INVALID_ARGUMENT;
       t@@ -84,9 +103,9 @@ ltk_parse_cmd(
                                parseinfo[i].initialized = 1;
                                break;
                        case CMDARG_ORIENTATION:
       -                        if (strcmp(tokens[i], "horizontal") == 0) {
       +                        if (strcmp(tokens[i].text, "horizontal") == 0) {
                                        parseinfo[i].val.orient = LTK_HORIZONTAL;
       -                        } else if (strcmp(tokens[i], "vertical") == 0) {
       +                        } else if (strcmp(tokens[i].text, "vertical") == 0) {
                                        parseinfo[i].val.orient = LTK_VERTICAL;
                                } else {
                                        err->type = ERR_INVALID_ARGUMENT;
       t@@ -97,7 +116,7 @@ ltk_parse_cmd(
                                break;
                        case CMDARG_BORDERSIDES:
                                parseinfo[i].val.border = LTK_BORDER_NONE;
       -                        for (const char *c = tokens[i]; *c != '\0'; c++) {
       +                        for (const char *c = tokens[i].text; *c != '\0'; c++) {
                                        switch (*c) {
                                        case 't':
                                                parseinfo[i].val.border |= LTK_BORDER_TOP;
   DIR diff --git a/src/cmd.h b/src/cmd.h
       t@@ -5,7 +5,8 @@
        
        typedef enum {
                CMDARG_IGNORE,
       -        CMDARG_STRING,
       +        CMDARG_STRING, /* nul-terminated string */
       +        CMDARG_DATA,   /* also char*, but may contain nul */
                CMDARG_COLOR,
                CMDARG_INT,
                CMDARG_BOOL,
       t@@ -21,9 +22,10 @@ typedef enum {
        typedef struct {
                ltk_cmdarg_datatype type;
                /* Note: Bool and int are both integers, but they are
       -           separate just to make it a bit clearer */
       +           separate just to make it a bit clearer (same goes for str/data) */
                union {
                        char *str;
       +                char *data;
                        ltk_color color;
                        int i;
                        int b;
       t@@ -32,6 +34,7 @@ typedef struct {
                        ltk_orientation orient;
                        ltk_widget *widget;
                } val;
       +        size_t len; /* only for data */
                int min, max; /* only for integers */ /* FIXME: which integer type is sensible here? */
                ltk_widget_type widget_type; /* only for widgets */
                int optional;
       t@@ -40,7 +43,14 @@ typedef struct {
        
        /* Returns 1 on error, 0 on success */
        /* All optional arguments must be in one block at the end */
       -int ltk_parse_cmd(ltk_window *window, char **tokens, size_t num_tokens, ltk_cmdarg_parseinfo *parseinfo, size_t num_arg, ltk_error *err);
       +int ltk_parse_cmd(
       +    ltk_window *window, ltk_cmd_token *tokens, size_t num_tokens,
       +    ltk_cmdarg_parseinfo *parseinfo, size_t num_arg, ltk_error *err
       +);
       +
       +#define GEN_CMD_HELPERS_PROTO(func_name)                                                                        \
       +int func_name(ltk_window *window, ltk_cmd_token *tokens, size_t num_tokens, ltk_error *err);
       +
        
        #define GEN_CMD_HELPERS(func_name, widget_type, widget_typename, array_name, entry_type)                        \
        /* FIXME: maybe just get rid of this and rely on array already being sorted? */                                        \
       t@@ -63,7 +73,7 @@ array_name##_sort_helper(const void *entry1v, const void *entry2v) {                                                \
        int                                                                                                                \
        func_name(                                                                                                        \
            ltk_window *window,                                                                                                \
       -    char **tokens,                                                                                                \
       +    ltk_cmd_token *tokens,                                                                                        \
            size_t num_tokens,                                                                                                \
            ltk_error *err) {                                                                                                \
                if (num_tokens < 3) {                                                                                        \
       t@@ -78,8 +88,17 @@ func_name(                                                                                                        \
                            sizeof(array_name[0]), &array_name##_sort_helper);                                                \
                        array_name##_sorted = 1;                                                                        \
                }                                                                                                        \
       +        if (tokens[1].contains_nul) {                                                                                \
       +                err->type = ERR_INVALID_ARGUMENT;                                                                \
       +                err->arg = 1;                                                                                        \
       +                return 1;                                                                                        \
       +        } else if (tokens[2].contains_nul) {                                                                        \
       +                err->type = ERR_INVALID_ARGUMENT;                                                                \
       +                err->arg = 2;                                                                                        \
       +                return 1;                                                                                        \
       +        }                                                                                                        \
                entry_type *e = bsearch(                                                                                \
       -            tokens[2], array_name, LENGTH(array_name),                                                                \
       +            tokens[2].text, array_name, LENGTH(array_name),                                                        \
                    sizeof(array_name[0]), &array_name##_search_helper                                                        \
                );                                                                                                        \
                if (!e) {                                                                                                \
       t@@ -90,7 +109,7 @@ func_name(                                                                                                        \
                if (e->needs_all) {                                                                                        \
                        return e->func(window, NULL, tokens, num_tokens, err);                                                \
                } else {                                                                                                \
       -                widget_typename *widget = (widget_typename *)ltk_get_widget(tokens[1], widget_type, err);        \
       +                widget_typename *widget = (widget_typename *)ltk_get_widget(tokens[1].text, widget_type, err);        \
                        if (!widget) {                                                                                        \
                                err->arg = 1;                                                                                \
                                return 1;                                                                                \
   DIR diff --git a/src/config.c b/src/config.c
       t@@ -673,10 +673,11 @@ ltk_config_parsefile(
                return ret;
        }
        
       +/* FIXME: update this */
        const char *default_config = "[general]\n"
        "explicit-focus = true\n"
        "all-activatable = true\n"
       -"[key-binding]\n"
       +"[key-binding:widget]\n"
        "bind-keypress move-next sym tab\n"
        "bind-keypress move-prev sym tab mods shift\n"
        "bind-keypress move-next text n\n"
   DIR diff --git a/src/entry.c b/src/entry.c
       t@@ -18,6 +18,7 @@
        /* FIXME: cursors jump weirdly with bidi text
           (need to support strong/weak cursors in pango backend) */
        /* FIXME: set imspot - needs to be standardized so widgets don't all do their own thing */
       +/* FIXME: some sort of width setting (setting a pixel width would be kind of ugly) */
        
        #include <stdio.h>
        #include <ctype.h>
       t@@ -792,7 +793,7 @@ static int
        ltk_entry_cmd_create(
            ltk_window *window,
            ltk_entry *entry_unneeded,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                (void)entry_unneeded;
       t@@ -817,7 +818,7 @@ ltk_entry_cmd_create(
        
        static struct entry_cmd {
                char *name;
       -        int (*func)(ltk_window *, ltk_entry *, char **, size_t, ltk_error *);
       +        int (*func)(ltk_window *, ltk_entry *, ltk_cmd_token *, size_t, ltk_error *);
                int needs_all;
        } entry_cmds[] = {
                {"create", &ltk_entry_cmd_create, 1},
   DIR diff --git a/src/entry.h b/src/entry.h
       t@@ -19,6 +19,7 @@
        
        /* Requires the following includes: <X11/Xlib.h>, "rect.h", "widget.h", "ltk.h", "color.h", "text.h" */
        
       +#include "cmd.h"
        #include "err.h"
        #include "config.h"
        
       t@@ -44,11 +45,6 @@ int ltk_entry_register_keypress(const char *func_name, size_t func_len, ltk_keyp
        int ltk_entry_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b);
        void ltk_entry_cleanup(void);
        
       -int ltk_entry_cmd(
       -    ltk_window *window,
       -    char **tokens,
       -    size_t num_tokens,
       -    ltk_error *
       -);
       +GEN_CMD_HELPERS_PROTO(ltk_entry_cmd)
        
        #endif /* LTK_ENTRY_H */
   DIR diff --git a/src/err.c b/src/err.c
       t@@ -11,6 +11,7 @@ static const char *errtable[] = {
                "Invalid widget id",
                "Invalid widget type",
                "Invalid command",
       +        "Unknown error",
                "Menu is not submenu",
                "Menu entry already contains submenu",
                "Invalid grid position",
   DIR diff --git a/src/err.h b/src/err.h
       t@@ -14,11 +14,12 @@ typedef enum {
                ERR_INVALID_WIDGET_ID = 7,
                ERR_INVALID_WIDGET_TYPE = 8,
                ERR_INVALID_COMMAND = 9,
       +        ERR_UNKNOWN = 10,
                /* widget specific */
       -        ERR_MENU_NOT_SUBMENU = 10,
       -        ERR_MENU_ENTRY_CONTAINS_SUBMENU = 11,
       -        ERR_GRID_INVALID_POSITION = 12,
       -        ERR_INVALID_SEQNUM = 13,
       +        ERR_MENU_NOT_SUBMENU = 11,
       +        ERR_MENU_ENTRY_CONTAINS_SUBMENU = 12,
       +        ERR_GRID_INVALID_POSITION = 13,
       +        ERR_INVALID_SEQNUM = 14,
        } ltk_errtype;
        
        typedef struct {
   DIR diff --git a/src/grid.c b/src/grid.c
       t@@ -1,4 +1,5 @@
        /* FIXME: sometimes, resizing doesn't work properly when running test.sh */
       +
        /*
         * Copyright (c) 2016-2023 lumidify <nobody@lumidify.org>
         *
       t@@ -47,7 +48,7 @@ static void ltk_grid_destroy(ltk_widget *self, int shallow);
        static void ltk_recalculate_grid(ltk_widget *self);
        static void ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget);
        static int ltk_grid_add(ltk_window *window, ltk_widget *widget, ltk_grid *grid,
       -    int row, int column, int row_span, int column_span, unsigned short sticky, ltk_error *err);
       +    int row, int column, int row_span, int column_span, ltk_sticky_mask sticky, ltk_error *err);
        static int ltk_grid_ungrid(ltk_widget *widget, ltk_widget *self, ltk_error *err);
        static int ltk_grid_find_nearest_column(ltk_grid *grid, int x);
        static int ltk_grid_find_nearest_row(ltk_grid *grid, int y);
       t@@ -97,31 +98,31 @@ static struct ltk_widget_vtable vtable = {
        static int ltk_grid_cmd_add(
            ltk_window *window,
            ltk_grid *grid,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err);
        static int ltk_grid_cmd_ungrid(
            ltk_window *window,
            ltk_grid *grid,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err);
        static int ltk_grid_cmd_create(
            ltk_window *window,
            ltk_grid *grid,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err);
        static int ltk_grid_cmd_set_row_weight(
            ltk_window *window,
            ltk_grid *grid,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err);
        static int ltk_grid_cmd_set_column_weight(
            ltk_window *window,
            ltk_grid *grid,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err);
        
       t@@ -146,7 +147,12 @@ ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
                        if (!grid->widget_grid[i])
                                continue;
                        ltk_widget *ptr = grid->widget_grid[i];
       -                ptr->vtable->draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, real_clip));
       +                int max_w = grid->column_pos[ptr->column + ptr->column_span] - grid->column_pos[ptr->column];
       +                int max_h = grid->row_pos[ptr->row + ptr->row_span] - grid->row_pos[ptr->row];
       +                ltk_rect r = ltk_rect_intersect(
       +                    (ltk_rect){grid->column_pos[ptr->column], grid->row_pos[ptr->row], max_w, max_h}, real_clip
       +                );
       +                ptr->vtable->draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, r));
                }
        }
        
       t@@ -239,6 +245,7 @@ ltk_recalculate_grid(ltk_widget *self) {
                                width_static += grid->column_widths[i];
                        }
                }
       +        /* FIXME: what should be done when static height or width is larger than grid? */
                if (total_row_weight > 0) {
                        height_unit = (float) (grid->widget.lrect.h - height_static) / (float) total_row_weight;
                }
       t@@ -270,13 +277,33 @@ ltk_recalculate_grid(ltk_widget *self) {
                                        continue;
                                /*orig_width = ptr->lrect.w;
                                orig_height = ptr->lrect.h;*/
       +                        ptr->lrect.w = ptr->ideal_w;
       +                        ptr->lrect.h = ptr->ideal_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->lrect.w = grid->column_pos[end_column] - grid->column_pos[j];
       -                        }
       -                        if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM) {
       -                                ptr->lrect.h = grid->row_pos[end_row] - grid->row_pos[i];
       +                        int max_w = grid->column_pos[end_column] - grid->column_pos[j];
       +                        int max_h = grid->row_pos[end_row] - grid->row_pos[i];
       +                        int stretch_width = (ptr->sticky & LTK_STICKY_LEFT) && (ptr->sticky & LTK_STICKY_RIGHT);
       +                        int shrink_width = (ptr->sticky & LTK_STICKY_SHRINK_WIDTH) && ptr->lrect.w > max_w;
       +                        int stretch_height = (ptr->sticky & LTK_STICKY_TOP) && (ptr->sticky & LTK_STICKY_BOTTOM);
       +                        int shrink_height = (ptr->sticky & LTK_STICKY_SHRINK_HEIGHT) && ptr->lrect.h > max_h;
       +                        if (stretch_width || shrink_width)
       +                                ptr->lrect.w = max_w;
       +                        if (stretch_height || shrink_height)
       +                                ptr->lrect.h = max_h;
       +                        if (ptr->sticky & LTK_STICKY_PRESERVE_ASPECT_RATIO) {
       +                                if (!stretch_width && !shrink_width) {
       +                                        ptr->lrect.w = (int)(((double)ptr->lrect.h / ptr->ideal_h) * ptr->ideal_w);
       +                                } else if (!stretch_height && !shrink_height) {
       +                                        ptr->lrect.h = (int)(((double)ptr->lrect.w / ptr->ideal_w) * ptr->ideal_h);
       +                                } else {
       +                                        double scale_w = (double)ptr->lrect.w / ptr->ideal_w;
       +                                        double scale_h = (double)ptr->lrect.h / ptr->ideal_h;
       +                                        if (scale_w * ptr->ideal_h > ptr->lrect.h)
       +                                                ptr->lrect.w = (int)(scale_h * ptr->ideal_w);
       +                                        else if (scale_h * ptr->ideal_w > ptr->lrect.w)
       +                                                ptr->lrect.h = (int)(scale_w * ptr->ideal_h);
       +                                }
                                }
                                /* FIXME: Figure out a better system for this - it would be nice to make it more
                                   efficient by not doing anything if nothing changed, but that doesn't work when
       t@@ -289,22 +316,27 @@ ltk_recalculate_grid(ltk_widget *self) {
                                /*if (orig_width != ptr->lrect.w || orig_height != ptr->lrect.h)*/
                                        ltk_widget_resize(ptr);
        
       -                        if (ptr->sticky & LTK_STICKY_RIGHT) {
       +                        /* the "default" case needs to come first because the widget may be stretched
       +                           with aspect ratio preserving, and in that case it should still be centered */
       +                        if (stretch_width || !(ptr->sticky & (LTK_STICKY_RIGHT|LTK_STICKY_LEFT))) {
       +                                ptr->lrect.x = grid->column_pos[j] + (grid->column_pos[end_column] - grid->column_pos[j] - ptr->lrect.w) / 2;
       +                        } else if (ptr->sticky & LTK_STICKY_RIGHT) {
                                        ptr->lrect.x = grid->column_pos[end_column] - ptr->lrect.w;
                                } else if (ptr->sticky & LTK_STICKY_LEFT) {
                                        ptr->lrect.x = grid->column_pos[j];
       -                        } else {
       -                                ptr->lrect.x = grid->column_pos[j] + ((grid->column_pos[end_column] - grid->column_pos[j]) / 2 - ptr->lrect.w / 2);
                                }
        
       -                        if (ptr->sticky & LTK_STICKY_BOTTOM) {
       +                        if (stretch_height || !(ptr->sticky & (LTK_STICKY_TOP|LTK_STICKY_BOTTOM))) {
       +                                ptr->lrect.y = grid->row_pos[i] + (grid->row_pos[end_row] - grid->row_pos[i] - ptr->lrect.h) / 2;
       +                        } else if (ptr->sticky & LTK_STICKY_BOTTOM) {
                                        ptr->lrect.y = grid->row_pos[end_row] - ptr->lrect.h;
                                } else if (ptr->sticky & LTK_STICKY_TOP) {
                                        ptr->lrect.y = grid->row_pos[i];
       -                        } else {
       -                                ptr->lrect.y = grid->row_pos[i] + ((grid->row_pos[end_row] - grid->row_pos[i]) / 2 - ptr->lrect.h / 2);
                                }
       +                        /* intersect both with the grid rect and with the rect of the covered cells since there may be
       +                           weird cases where the layout doesn't work properly and the cells are partially outside the grid */
                                ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
       +                        ptr->crect = ltk_rect_intersect((ltk_rect){grid->column_pos[j], grid->row_pos[i], max_w, max_h}, ptr->crect);
                        }
                }
        }
       t@@ -341,7 +373,7 @@ ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget) {
        /* FIXME: Check if widget already exists at position */
        static int
        ltk_grid_add(ltk_window *window, ltk_widget *widget, ltk_grid *grid,
       -    int row, int column, int row_span, int column_span, unsigned short sticky, ltk_error *err) {
       +    int row, int column, int row_span, int column_span, ltk_sticky_mask sticky, ltk_error *err) {
                if (widget->parent) {
                        err->type = ERR_WIDGET_IN_CONTAINER;
                        return 1;
       t@@ -543,7 +575,7 @@ static int
        ltk_grid_cmd_add(
            ltk_window *window,
            ltk_grid *grid,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -574,7 +606,7 @@ static int
        ltk_grid_cmd_ungrid(
            ltk_window *window,
            ltk_grid *grid,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -595,7 +627,7 @@ static int
        ltk_grid_cmd_create(
            ltk_window *window,
            ltk_grid *grid_unneeded,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                (void)grid_unneeded;
       t@@ -625,7 +657,7 @@ static int
        ltk_grid_cmd_set_row_weight(
            ltk_window *window,
            ltk_grid *grid,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -647,7 +679,7 @@ static int
        ltk_grid_cmd_set_column_weight(
            ltk_window *window,
            ltk_grid *grid,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -663,7 +695,7 @@ ltk_grid_cmd_set_column_weight(
        
        static struct grid_cmd {
                char *name;
       -        int (*func)(ltk_window *, ltk_grid *, char **, size_t, ltk_error *);
       +        int (*func)(ltk_window *, ltk_grid *, ltk_cmd_token *, size_t, ltk_error *);
                int needs_all;
        } grid_cmds[] = {
                {"add", &ltk_grid_cmd_add, 0},
   DIR diff --git a/src/grid.h b/src/grid.h
       t@@ -1,5 +1,5 @@
        /*
       - * Copyright (c) 2016, 2017, 2018, 2020 lumidify <nobody@lumidify.org>
       + * Copyright (c) 2016-2023 lumidify <nobody@lumidify.org>
         *
         * Permission to use, copy, modify, and/or distribute this software for any
         * purpose with or without fee is hereby granted, provided that the above
       t@@ -19,6 +19,7 @@
        
        /* Requires the following includes: "rect.h", "widget.h", "ltk.h" */
        
       +#include "cmd.h"
        #include "err.h"
        
        /*
       t@@ -26,8 +27,6 @@
         */
        typedef struct {
                ltk_widget widget;
       -        int rows;
       -        int columns;
                ltk_widget **widget_grid;
                int *row_heights;
                int *column_widths;
       t@@ -35,8 +34,10 @@ typedef struct {
                int *column_weights;
                int *row_pos;
                int *column_pos;
       +        int rows;
       +        int columns;
        } ltk_grid;
        
       -int ltk_grid_cmd(ltk_window *window, char **tokens, size_t num_tokens, ltk_error *err);
       +GEN_CMD_HELPERS_PROTO(ltk_grid_cmd)
        
        #endif /* LTK_GRID_H */
   DIR diff --git a/src/image.c b/src/image.c
       t@@ -0,0 +1,112 @@
       +/*
       + * Copyright (c) 2023 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <Imlib2.h>
       +
       +#include "ltk.h"
       +#include "image.h"
       +#include "memory.h"
       +#include "graphics.h"
       +#include "xlib_shared.h"
       +
       +struct ltk_image {
       +        Imlib_Image img;
       +};
       +
       +void
       +ltk_image_init(ltk_renderdata *data, size_t cache_bytes) {
       +        /* FIXME: overflow */
       +        imlib_set_cache_size((int)cache_bytes);
       +        imlib_set_color_usage(128);
       +        imlib_context_set_blend(0);
       +        imlib_context_set_dither(1);
       +        imlib_context_set_display(data->dpy);
       +        imlib_context_set_visual(data->vis);
       +        imlib_context_set_colormap(data->cm);
       +}
       +
       +ltk_image *
       +ltk_image_create_from_mem(const char *path, const char *data, size_t sz) {
       +        Imlib_Image img = imlib_load_image_mem(path, data, sz);
       +        if (!img)
       +                return NULL;
       +        ltk_image *image = ltk_malloc(sizeof(ltk_image));
       +        image->img = img;
       +        return image;
       +}
       +
       +ltk_image *
       +ltk_image_create_cropped_scaled(
       +    ltk_image *image, int src_x, int src_y,
       +    int src_w, int src_h, int dst_w, int dst_h) {
       +        imlib_context_set_image(image->img);
       +        /* FIXME: check since when supported */
       +        Imlib_Image img = imlib_create_cropped_scaled_image(
       +            src_x, src_y, src_w, src_h, dst_w, dst_h
       +        );
       +        if (!img)
       +                return NULL;
       +        ltk_image *new_image = ltk_malloc(sizeof(ltk_image));
       +        new_image->img = img;
       +        return new_image;
       +}
       +
       +void
       +ltk_image_draw(ltk_image *image, ltk_surface *draw_surf, int x, int y) {
       +        imlib_context_set_image(image->img);
       +        imlib_context_set_drawable(ltk_surface_get_drawable(draw_surf));
       +        imlib_render_image_on_drawable(x, y);
       +}
       +
       +void
       +ltk_image_draw_scaled(
       +    ltk_image *image, ltk_surface *draw_surf,
       +    int x, int y, int w, int h) {
       +        imlib_context_set_image(image->img);
       +        imlib_context_set_drawable(ltk_surface_get_drawable(draw_surf));
       +        imlib_render_image_on_drawable_at_size(x, y, w, h);
       +}
       +
       +void
       +ltk_image_draw_cropped_scaled(
       +    ltk_image *image, ltk_surface *draw_surf,
       +    int src_x, int src_y, int src_w, int src_h,
       +    int dst_x, int dst_y, int dst_w, int dst_h) {
       +        imlib_context_set_image(image->img);
       +        imlib_context_set_drawable(ltk_surface_get_drawable(draw_surf));
       +        imlib_render_image_part_on_drawable_at_size(
       +            src_x, src_y, src_w, src_h, dst_x, dst_y, dst_w, dst_h
       +        );
       +}
       +
       +int
       +ltk_image_get_width(ltk_image *image) {
       +        imlib_context_set_image(image->img);
       +        return imlib_image_get_width();
       +}
       +
       +int
       +ltk_image_get_height(ltk_image *image) {
       +        imlib_context_set_image(image->img);
       +        return imlib_image_get_height();
       +}
       +
       +void
       +ltk_image_destroy(ltk_image *image) {
       +        imlib_context_set_image(image->img);
       +        imlib_free_image();
       +        ltk_free(image);
       +}
   DIR diff --git a/src/image.h b/src/image.h
       t@@ -0,0 +1,48 @@
       +/*
       + * Copyright (c) 2023 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTK_IMAGE_H
       +#define LTK_IMAGE_H
       +
       +/* FIXME: Files including this need to include ltk.h because that's
       +   currently where the typedef for ltk_surface is. That's really ugly. */
       +#include "graphics.h"
       +
       +typedef struct ltk_image ltk_image;
       +
       +void ltk_image_init(ltk_renderdata *data, size_t cache_bytes);
       +/* path is only used to guess file type */
       +/* data is not taken over! */
       +ltk_image *ltk_image_create_from_mem(const char *path, const char *data, size_t sz);
       +ltk_image *ltk_image_create_cropped_scaled(
       +    ltk_image *image, int src_x, int src_y,
       +    int src_w, int src_h, int dst_w, int dst_h
       +);
       +void ltk_image_draw(ltk_image *image, ltk_surface *draw_surf, int x, int y);
       +void ltk_image_draw_scaled(
       +    ltk_image *image, ltk_surface *draw_surf,
       +    int x, int y, int w, int h
       +);
       +void ltk_image_draw_cropped_scaled(
       +    ltk_image *image, ltk_surface *draw_surf,
       +    int src_x, int src_y, int src_w, int src_h,
       +    int dst_x, int dst_y, int dst_w, int dst_h
       +);
       +int ltk_image_get_width(ltk_image *image);
       +int ltk_image_get_height(ltk_image *image);
       +void ltk_image_destroy(ltk_image *image);
       +
       +#endif /* LTK_IMAGE_H */
   DIR diff --git a/src/image_widget.c b/src/image_widget.c
       t@@ -0,0 +1,154 @@
       +/*
       + * Copyright (c) 2023 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <stdint.h>
       +#include <string.h>
       +#include <stdarg.h>
       +
       +#include "event.h"
       +#include "memory.h"
       +#include "color.h"
       +#include "rect.h"
       +#include "widget.h"
       +#include "ltk.h"
       +#include "util.h"
       +#include "image_widget.h"
       +#include "graphics.h"
       +#include "cmd.h"
       +
       +/* FIXME: add padding, etc. */
       +
       +static void ltk_image_widget_draw(
       +    ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip
       +);
       +static ltk_image_widget *ltk_image_widget_create(
       +    ltk_window *window, const char *id, const char *path, const char *data, size_t len
       +);
       +static void ltk_image_widget_destroy(ltk_widget *self, int shallow);
       +
       +static struct ltk_widget_vtable vtable = {
       +        .draw = &ltk_image_widget_draw,
       +        .destroy = &ltk_image_widget_destroy,
       +        .hide = NULL,
       +        .resize = NULL,
       +        .change_state = NULL,
       +        .child_size_change = NULL,
       +        .remove_child = NULL,
       +        .get_child_at_pos = NULL,
       +        .key_press = NULL,
       +        .key_release = NULL,
       +        .mouse_press = NULL,
       +        .mouse_release = NULL,
       +        .motion_notify = NULL,
       +        .mouse_leave = NULL,
       +        .mouse_enter = NULL,
       +        .type = LTK_WIDGET_IMAGE,
       +        .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_SPECIAL,
       +};
       +
       +static void
       +ltk_image_widget_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
       +        ltk_image_widget *img = (ltk_image_widget *)self;
       +        ltk_rect lrect = self->lrect;
       +        ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
       +        if (clip_final.w <= 0 || clip_final.h <= 0)
       +                return;
       +        /* FIXME: maybe replace with integer operations */
       +        double scalex = self->ideal_w / (double)lrect.w;
       +        double scaley = self->ideal_h / (double)lrect.h;
       +        int src_x = (int)(clip_final.x * scalex);
       +        int src_y = (int)(clip_final.y * scaley);
       +        int src_w = (int)(clip_final.w * scalex);
       +        int src_h = (int)(clip_final.h * scaley);
       +        ltk_image_draw_cropped_scaled(
       +            img->img, draw_surf, src_x, src_y, src_w, src_h,
       +            x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h
       +        );
       +}
       +
       +static ltk_image_widget *
       +ltk_image_widget_create(ltk_window *window, const char *id, const char *path, const char *data, size_t len) {
       +        ltk_image_widget *img = ltk_malloc(sizeof(ltk_image_widget));
       +        img->img = ltk_image_create_from_mem(path, data, len);
       +        if (!img->img)
       +                return NULL;
       +        int w = ltk_image_get_width(img->img);
       +        int h = ltk_image_get_height(img->img);
       +        ltk_fill_widget_defaults(&img->widget, id, window, &vtable, w, h);
       +        img->widget.ideal_w = w;
       +        img->widget.ideal_h = h;
       +
       +        return img;
       +}
       +
       +static void
       +ltk_image_widget_destroy(ltk_widget *self, int shallow) {
       +        (void)shallow;
       +        ltk_image_widget *img = (ltk_image_widget *)self;
       +        if (!img) {
       +                ltk_warn("Tried to destroy NULL image widget.\n");
       +                return;
       +        }
       +        ltk_image_destroy(img->img);
       +        ltk_free(img);
       +}
       +
       +/* image <image id> create <filename> <data> */
       +static int
       +ltk_image_widget_cmd_create(
       +    ltk_window *window,
       +    ltk_image_widget *img_unneeded,
       +    ltk_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltk_error *err) {
       +        (void)img_unneeded;
       +        ltk_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_DATA, .optional = 0},
       +        };
       +        if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        if (!ltk_widget_id_free(cmd[1].val.str)) {
       +                err->type = ERR_WIDGET_ID_IN_USE;
       +                err->arg = 1;
       +                return 1;
       +        }
       +        ltk_image_widget *img = ltk_image_widget_create(window, cmd[1].val.str, cmd[3].val.str, cmd[4].val.data, cmd[4].len);
       +        if (!img) {
       +                /* FIXME: more sensible error name */
       +                err->type = ERR_UNKNOWN;
       +                err->arg = -1;
       +                return 1;
       +        }
       +        ltk_set_widget((ltk_widget *)img, cmd[1].val.str);
       +
       +        return 0;
       +}
       +
       +static struct image_widget_cmd {
       +        char *name;
       +        int (*func)(ltk_window *, ltk_image_widget *, ltk_cmd_token *, size_t, ltk_error *);
       +        int needs_all;
       +} image_widget_cmds[] = {
       +        {"create", &ltk_image_widget_cmd_create, 1},
       +};
       +
       +GEN_CMD_HELPERS(ltk_image_widget_cmd, LTK_WIDGET_IMAGE, ltk_image_widget, image_widget_cmds, struct image_widget_cmd)
   DIR diff --git a/src/image_widget.h b/src/image_widget.h
       t@@ -0,0 +1,31 @@
       +/*
       + * Copyright (c) 2023 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTK_IMAGE_WIDGET_H
       +#define LTK_IMAGE_WIDGET_H
       +
       +#include "cmd.h"
       +#include "err.h"
       +#include "image.h"
       +
       +typedef struct {
       +        ltk_widget widget;
       +        ltk_image *img;
       +} ltk_image_widget;
       +
       +GEN_CMD_HELPERS_PROTO(ltk_image_widget_cmd)
       +
       +#endif /* LTK_IMAGE_WIDGET_H */
   DIR diff --git a/src/label.c b/src/label.c
       t@@ -129,6 +129,7 @@ ltk_label_create(ltk_window *window, const char *id, char *text) {
                label->tl = ltk_text_line_create(window->text_context, font_size, text, 0, -1);
                int text_w, text_h;
                ltk_text_line_get_size(label->tl, &text_w, &text_h);
       +        /* FIXME: what was I even thinking here? label->widget.ideal_{w,h} isn't even initialized here */
                ltk_fill_widget_defaults(&label->widget, id, window, &vtable, label->widget.ideal_w, label->widget.ideal_h);
                label->widget.ideal_w = text_w + theme.pad * 2;
                label->widget.ideal_h = text_h + theme.pad * 2;
       t@@ -155,7 +156,7 @@ static int
        ltk_label_cmd_create(
            ltk_window *window,
            ltk_label *label_unneeded,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                (void)label_unneeded;
       t@@ -180,7 +181,7 @@ ltk_label_cmd_create(
        
        static struct label_cmd {
                char *name;
       -        int (*func)(ltk_window *, ltk_label *, char **, size_t, ltk_error *);
       +        int (*func)(ltk_window *, ltk_label *, ltk_cmd_token *, size_t, ltk_error *);
                int needs_all;
        } label_cmds[] = {
                {"create", &ltk_label_cmd_create, 1},
   DIR diff --git a/src/label.h b/src/label.h
       t@@ -19,6 +19,7 @@
        
        /* Requires the following includes: <X11/Xlib.h>, "rect.h", "widget.h", "ltk.h", "color.h", "text.h" */
        
       +#include "cmd.h"
        #include "err.h"
        
        typedef struct {
       t@@ -31,11 +32,6 @@ int ltk_label_ini_handler(ltk_window *window, const char *prop, const char *valu
        int ltk_label_fill_theme_defaults(ltk_window *window);
        void ltk_label_uninitialize_theme(ltk_window *window);
        
       -int ltk_label_cmd(
       -    ltk_window *window,
       -    char **tokens,
       -    size_t num_tokens,
       -    ltk_error *err
       -);
       +GEN_CMD_HELPERS_PROTO(ltk_label_cmd)
        
        #endif /* LTK_LABEL_H */
   DIR diff --git a/src/ltk.h b/src/ltk.h
       t@@ -1,5 +1,5 @@
        /*
       - * Copyright (c) 2016, 2017, 2018, 2022 lumidify <nobody@lumidify.org>
       + * Copyright (c) 2016-2023 lumidify <nobody@lumidify.org>
         *
         * Permission to use, copy, modify, and/or distribute this software for any
         * purpose with or without fee is hereby granted, provided that the above
       t@@ -18,8 +18,15 @@
        #define _LTK_H_
        
        #include <stdint.h>
       +#include <stdlib.h>
        #include "proto_types.h"
        
       +typedef struct {
       +        char *text;
       +        size_t len;
       +        int contains_nul;
       +} ltk_cmd_token;
       +
        typedef enum {
                LTK_EVENT_RESIZE = 1 << 0,
                LTK_EVENT_BUTTON = 1 << 1,
   DIR diff --git a/src/ltkc.c b/src/ltkc.c
       t@@ -1,5 +1,5 @@
        /*
       - * Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org>
       + * Copyright (c) 2021-2023 lumidify <nobody@lumidify.org>
         *
         * Permission to use, copy, modify, and/or distribute this software for any
         * purpose with or without fee is hereby granted, provided that the above
       t@@ -28,6 +28,7 @@
        #include <sys/un.h>
        #include "util.h"
        #include "memory.h"
       +#include "macros.h"
        
        #define BLK_SIZE 128
        char tmp_buf[BLK_SIZE];
       t@@ -47,16 +48,18 @@ static int sockfd = -1;
        
        int main(int argc, char *argv[]) {
                char num[12];
       +        int bs = 0;
                int last_newline = 1;
       +        int in_str = 0;
                uint32_t seq = 0;
       -        int maxrfd, maxwfd;
       +        int maxfd;
                int infd = fileno(stdin);
                int outfd = fileno(stdout);
                struct sockaddr_un un;
                fd_set rfds, wfds, rallfds, wallfds;
       -        struct timeval tv, tv_master;
       -        tv_master.tv_sec = 0;
       -        tv_master.tv_usec = 20000;
       +        struct timeval tv;
       +        tv.tv_sec = 0;
       +        tv.tv_usec = 20000;
                size_t path_size;
        
                if (argc != 2) {
       t@@ -90,6 +93,16 @@ int main(int argc, char *argv[]) {
                        perror("Socket error");
                        return -2;
                }
       +        if (set_nonblock(sockfd)) {
       +                (void)fprintf(stderr, "Unable to set socket to non-blocking mode.\n");
       +                return 1;
       +        } else if (set_nonblock(infd)) {
       +                (void)fprintf(stderr, "Unable to set stdin to non-blocking mode.\n");
       +                return 1;
       +        } else if (set_nonblock(outfd)) {
       +                (void)fprintf(stderr, "Unable to set stdout to non-blocking mode.\n");
       +                return 1;
       +        }
        
                io_buffers.in_buffer = ltk_malloc(BLK_SIZE);
                io_buffers.in_alloc = BLK_SIZE;
       t@@ -104,105 +117,129 @@ int main(int argc, char *argv[]) {
                FD_SET(infd, &rallfds);
                FD_SET(sockfd, &wallfds);
                FD_SET(outfd, &wallfds);
       -        maxrfd = sockfd > infd ? sockfd : infd;
       -        maxwfd = sockfd > outfd ? sockfd : outfd;
       +        maxfd = sockfd > infd ? sockfd : infd;
       +        if (maxfd < outfd)
       +                maxfd = outfd;
       +
       +        struct timespec now, elapsed, last, sleep_time;
       +        clock_gettime(CLOCK_MONOTONIC, &last);
       +        sleep_time.tv_sec = 0;
        
                while (1) {
       -                if (!FD_ISSET(infd, &rallfds) &&
       -                    !FD_ISSET(sockfd, &rallfds) &&
       -                    io_buffers.in_len == 0 &&
       -                    io_buffers.out_len == 0)
       +                if (!FD_ISSET(sockfd, &rallfds) && 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 */
       -                tv = tv_master;
       -                select(maxrfd + 1, &rfds, NULL, NULL, &tv);
       -                /* value of tv doesn't really matter anymore here because the
       -                   necessary framerate-limiting delay is already done */
       -                select(maxwfd + 1, NULL, &wfds, NULL, &tv);
       +                select(maxfd + 1, &rfds, &wfds, NULL, &tv);
        
       +                /* FIXME: make all this buffer handling a bit more intelligent */
                        if (FD_ISSET(sockfd, &rfds)) {
       -                        ltk_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;
       +                        while (1) {
       +                                ltk_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) {
       +                                        /* FIXME: distinguish errors */
       +                                        break;
       +                                } else if (nread == 0) {
       +                                        FD_CLR(sockfd, &rallfds);
       +                                        FD_CLR(sockfd, &wallfds);
       +                                        break;
       +                                } else {
       +                                        io_buffers.out_len += nread;
       +                                }
                                }
                        }
        
                        if (FD_ISSET(infd, &rfds)) {
       -                        int nread = read(infd, tmp_buf, BLK_SIZE);
       -                        if (nread < 0) {
       -                                return 2;
       -                        } else if (nread == 0) {
       -                                FD_CLR(infd, &rallfds);
       -                        } else {
       -                                for (int i = 0; i < nread; i++) {
       -                                        if (last_newline) {
       -                                                int numlen = snprintf(num, sizeof(num), "%"PRIu32" ", seq);
       -                                                if (numlen < 0 || (unsigned)numlen >= sizeof(num))
       -                                                        ltk_fatal("There's a bug in the universe.\n");
       -                                                ltk_grow_string(
       -                                                    &io_buffers.in_buffer,
       -                                                    &io_buffers.in_alloc,
       -                                                    io_buffers.in_len + numlen
       -                                                );
       -                                                memcpy(io_buffers.in_buffer + io_buffers.in_len, num, numlen);
       -                                                io_buffers.in_len += numlen;
       -                                                last_newline = 0;
       -                                                seq++;
       -                                        }
       -                                        if (tmp_buf[i] == '\n')
       -                                                last_newline = 1;
       -                                        if (io_buffers.in_len == io_buffers.in_alloc) {
       -                                                ltk_grow_string(
       -                                                    &io_buffers.in_buffer,
       -                                                    &io_buffers.in_alloc,
       -                                                    io_buffers.in_len + 1
       -                                                );
       +                        while (1) {
       +                                int nread = read(infd, tmp_buf, BLK_SIZE);
       +                                if (nread < 0) {
       +                                        break;
       +                                } else if (nread == 0) {
       +                                        FD_CLR(infd, &rallfds);
       +                                        break;
       +                                } else {
       +                                        for (int i = 0; i < nread; i++) {
       +                                                if (last_newline) {
       +                                                        int numlen = snprintf(num, sizeof(num), "%"PRIu32" ", seq);
       +                                                        if (numlen < 0 || (unsigned)numlen >= sizeof(num))
       +                                                                ltk_fatal("There's a bug in the universe.\n");
       +                                                        ltk_grow_string(
       +                                                            &io_buffers.in_buffer,
       +                                                            &io_buffers.in_alloc,
       +                                                            io_buffers.in_len + numlen
       +                                                        );
       +                                                        memcpy(io_buffers.in_buffer + io_buffers.in_len, num, numlen);
       +                                                        io_buffers.in_len += numlen;
       +                                                        last_newline = 0;
       +                                                        seq++;
       +                                                }
       +                                                if (tmp_buf[i] == '\\') {
       +                                                        bs++;
       +                                                        bs %= 2;
       +                                                } else if (tmp_buf[i] == '"' && !bs) {
       +                                                        in_str = !in_str;
       +                                                } else if (tmp_buf[i] == '\n' && !in_str) {
       +                                                        last_newline = 1;
       +                                                } else {
       +                                                        bs = 0;
       +                                                }
       +                                                if (io_buffers.in_len == io_buffers.in_alloc) {
       +                                                        ltk_grow_string(
       +                                                            &io_buffers.in_buffer,
       +                                                            &io_buffers.in_alloc,
       +                                                            io_buffers.in_len + 1
       +                                                        );
       +                                                }
       +                                                io_buffers.in_buffer[io_buffers.in_len++] = tmp_buf[i];
                                                }
       -                                        io_buffers.in_buffer[io_buffers.in_len++] = tmp_buf[i];
                                        }
                                }
                        }
        
                        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;
       +                        while (io_buffers.in_len > 0) {
       +                                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) {
       +                                        break;
       +                                } 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;
       +                        while (io_buffers.out_len > 0) {
       +                                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) {
       +                                        break;
       +                                } else {
       +                                        memmove(io_buffers.out_buffer,
       +                                                io_buffers.out_buffer + nwritten,
       +                                                io_buffers.out_len - nwritten);
       +                                        io_buffers.out_len -= nwritten;
       +                                }
                                }
                        }
       +                clock_gettime(CLOCK_MONOTONIC, &now);
       +                ltk_timespecsub(&now, &last, &elapsed);
       +                /* FIXME: configure framerate */
       +                if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
       +                        sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
       +                        nanosleep(&sleep_time, NULL);
       +                }
       +                last = now;
                }
        
                ltk_cleanup();
   DIR diff --git a/src/ltkc_img.c b/src/ltkc_img.c
       t@@ -0,0 +1,26 @@
       +/* This is just a temporary hack to preprocess an image for sending it to
       +   ltkd. The nicer way for this would be to have a special case for the
       +   "image create" command in ltkc, but I was too lazy to implement that
       +   right now. */
       +
       +#include <stdio.h>
       +
       +int main(int argc, char *argv[]) {
       +        (void)argc;
       +        (void)argv;
       +        int c;
       +        while ((c = getchar()) != EOF) {
       +                switch (c) {
       +                case '\\':
       +                        fputs("\\\\", stdout);
       +                        break;
       +                case '"':
       +                        fputs("\\\"", stdout);
       +                        break;
       +                default:
       +                        putchar(c);
       +                }
       +        }
       +
       +        return 0;
       +}
   DIR diff --git a/src/ltkd.c b/src/ltkd.c
       t@@ -1,10 +1,8 @@
       -/* FIXME: backslashes should be parsed properly! */
        /* FIXME: Figure out how to properly print window id */
        /* FIXME: error checking in tokenizer (is this necessary?) */
       -/* FIXME: parsing doesn't work properly with bs? */
        /* FIXME: strip whitespace at end of lines in socket format */
        /*
       - * Copyright (c) 2016-2018, 2020-2023 lumidify <nobody@lumidify.org>
       + * Copyright (c) 2016-2023 lumidify <nobody@lumidify.org>
         *
         * Permission to use, copy, modify, and/or distribute this software for any
         * purpose with or without fee is hereby granted, provided that the above
       t@@ -60,6 +58,8 @@
        #include "scrollbar.h"
        #include "box.h"
        #include "menu.h"
       +#include "image.h"
       +#include "image_widget.h"
        #include "macros.h"
        #include "config.h"
        
       t@@ -70,11 +70,13 @@
        #define WRITE_BLK_SIZE 128
        
        struct token_list {
       -        char **tokens;
       +        ltk_cmd_token *tokens;
       +        /* FIXME: size_t everywhere */
                int num_tokens;
                int num_alloc;
        };
        
       +/* FIXME: switch to size_t */
        static struct ltk_sock_info {
                int fd;                    /* file descriptor for socket connection */
                int event_mask;            /* events to send to socket */
       t@@ -132,7 +134,7 @@ static int push_token(struct token_list *tl, char *token);
        static int read_sock(struct ltk_sock_info *sock);
        static int write_sock(struct ltk_sock_info *sock);
        static int tokenize_command(struct ltk_sock_info *sock);
       -static int ltk_set_root_widget_cmd(ltk_window *window, char **tokens, int num_tokens, ltk_error *err);
       +static int ltk_set_root_widget_cmd(ltk_window *window, ltk_cmd_token *tokens, int num_tokens, ltk_error *err);
        static int process_commands(ltk_window *window, int client);
        static int add_client(int fd);
        static int listen_sock(const char *sock_path);
       t@@ -156,7 +158,7 @@ typedef struct {
                int (*register_keypress)(const char *, size_t, ltk_keypress_binding);
                int (*register_keyrelease)(const char *, size_t, ltk_keyrelease_binding);
                void (*cleanup)(void);
       -        int (*cmd)(ltk_window *, char **, size_t, ltk_error *);
       +        int (*cmd)(ltk_window *, ltk_cmd_token *, size_t, ltk_error *);
        } ltk_widget_funcs;
        
        /* FIXME: use binary search when searching for the widget */
       t@@ -212,6 +214,16 @@ ltk_widget_funcs widget_funcs[] = {
                        .cmd = &ltk_label_cmd
                },
                {
       +                .name = "image",
       +                .ini_handler = NULL,
       +                .fill_theme_defaults = NULL,
       +                .uninitialize_theme = NULL,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +                .cmd = &ltk_image_widget_cmd
       +        },
       +        {
                        .name = "menu",
                        .ini_handler = &ltk_menu_ini_handler,
                        .fill_theme_defaults = &ltk_menu_fill_theme_defaults,
       t@@ -346,6 +358,9 @@ static struct {
                int listenfd;
        } sock_state;
        
       +/* FIXME: this is extremely dangerous right now because pretty much any command
       +   can be executed, so for instance the widget that caused the lock could also
       +   be destroyed, causing issues when this function returns */
        int
        ltk_handle_lock_client(ltk_window *window, int client) {
                if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
       t@@ -356,24 +371,30 @@ ltk_handle_lock_client(ltk_window *window, int client) {
                FD_ZERO(&wallfds);
                FD_SET(clifd, &rallfds);
                FD_SET(clifd, &wallfds);
       -        int rretval, wretval;
       -        struct timeval tv, tv_master;
       -        tv_master.tv_sec = 0;
       -        tv_master.tv_usec = 20000;
       +        int retval;
       +        struct timeval tv;
       +        tv.tv_sec = 0;
       +        tv.tv_usec = 0;
       +        struct timespec now, elapsed, last, sleep_time;
       +        clock_gettime(CLOCK_MONOTONIC, &last);
       +        sleep_time.tv_sec = 0;
                while (1) {
                        rfds = rallfds;
                        wfds = wallfds;
       -                /* separate these because the writing fds are usually
       -                   always ready for writing */
       -                tv = tv_master;
       -                rretval = select(clifd + 1, &rfds, NULL, NULL, &tv);
       -                /* value of tv doesn't really matter anymore here because the
       -                   necessary framerate-limiting delay is already done */
       -                wretval = select(clifd + 1, NULL, &wfds, NULL, &tv);
       -
       -                if (rretval > 0 || ((sockets[client].write_cur != sockets[client].write_len) && wretval > 0)) {
       +                retval = select(clifd + 1, &rfds, &wfds, NULL, &tv);
       +
       +                if (retval > 0) {
                                if (FD_ISSET(clifd, &rfds)) {
       -                                if (read_sock(&sockets[client]) == 0) {
       +                                int ret;
       +                                while ((ret = read_sock(&sockets[client])) == 1) {
       +                                        int pret;
       +                                        if ((pret = process_commands(window, client)) == 1)
       +                                                return 1;
       +                                        else if (pret == -1)
       +                                                return 0;
       +                                }
       +                                /* FIXME: maybe also return on read error? or would that be dangerous? */
       +                                if (ret == 0) {
                                                FD_CLR(clifd, &sock_state.rallfds);
                                                FD_CLR(clifd, &sock_state.wallfds);
                                                ltk_widget_remove_client(client);
       t@@ -390,18 +411,21 @@ ltk_handle_lock_client(ltk_window *window, int client) {
                                                        break;
                                                }
                                                return 0;
       -                                } else {
       -                                        int ret;
       -                                        if ((ret = process_commands(window, client)) == 1)
       -                                                return 1;
       -                                        else if (ret == -1)
       -                                                return 0;
                                        }
                                }
                                if (FD_ISSET(clifd, &wfds)) {
       +                                /* FIXME: call in loop like above */
                                        write_sock(&sockets[client]);
                                }
                        }
       +                clock_gettime(CLOCK_MONOTONIC, &now);
       +                ltk_timespecsub(&now, &last, &elapsed);
       +                /* FIXME: configure framerate */
       +                if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
       +                        sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
       +                        nanosleep(&sleep_time, NULL);
       +                }
       +                last = now;
                }
                return 0;
        }
       t@@ -410,12 +434,11 @@ static int
        ltk_mainloop(ltk_window *window) {
                ltk_event event;
                fd_set rfds, wfds;
       -        int rretval, wretval;
       +        int retval;
                int clifd;
       -        struct timeval tv, tv_master;
       -        tv_master.tv_sec = 0;
       -        /* FIXME: configure this number */
       -        tv_master.tv_usec = 20000;
       +        struct timeval tv;
       +        tv.tv_sec = 0;
       +        tv.tv_usec = 0;
        
                FD_ZERO(&sock_state.rallfds);
                FD_ZERO(&sock_state.wallfds);
       t@@ -434,10 +457,10 @@ ltk_mainloop(ltk_window *window) {
                /* FIXME: make time management smarter - maybe always figure out how long
                   it will take until the next timer is due and then sleep if no other events
                   are happening */
       -        struct timespec now, elapsed, last;
       +        struct timespec now, elapsed, last, lasttimer, sleep_time;
                clock_gettime(CLOCK_MONOTONIC, &last);
       -
       -        /* FIXME: framerate limiting for draw */
       +        lasttimer = last;
       +        sleep_time.tv_sec = 0;
        
                /* initialize keyboard mapping */
                ltk_generate_keyboard_event(window->renderdata, &event);
       t@@ -473,17 +496,11 @@ ltk_mainloop(ltk_window *window) {
                        }
                        rfds = sock_state.rallfds;
                        wfds = sock_state.wallfds;
       -                /* separate these because the writing fds are usually
       -                   always ready for writing */
       -                tv = tv_master;
       -                rretval = select(sock_state.maxfd + 1, &rfds, NULL, NULL, &tv);
       -                /* value of tv doesn't really matter anymore here because the
       -                   necessary framerate-limiting delay is already done */
       -                wretval = select(sock_state.maxfd + 1, NULL, &wfds, NULL, &tv);
       +                retval = select(sock_state.maxfd + 1, &rfds, &wfds, NULL, &tv);
                        while (!ltk_next_event(window->renderdata, window->clipboard, window->cur_kbd, &event))
                                ltk_handle_event(window, &event);
        
       -                if (rretval > 0 || (sock_write_available && wretval > 0)) {
       +                if (retval > 0) {
                                if (FD_ISSET(sock_state.listenfd, &rfds)) {
                                        if ((clifd = accept_sock(sock_state.listenfd)) < 0) {
                                                /* FIXME: Just log this! */
       t@@ -502,11 +519,23 @@ ltk_mainloop(ltk_window *window) {
                                        if ((clifd = sockets[i].fd) < 0)
                                                continue;
                                        if (FD_ISSET(clifd, &rfds)) {
       -                                        if (read_sock(&sockets[i]) == 0) {
       +                                        /* FIXME: better error handling - this assumes error
       +                                           is always because read would block */
       +                                        /* FIXME: maybe maximum number of iterations here to
       +                                           avoid choking on a lot of data? although such a
       +                                           large amount of data would probably cause other
       +                                           problems anyways */
       +                                        /* or maybe measure time and break after max time? */
       +                                        int ret;
       +                                        while ((ret = read_sock(&sockets[i])) == 1) {
       +                                                process_commands(window, i);
       +                                        }
       +                                        if (ret == 0) {
                                                        ltk_widget_remove_client(i);
                                                        FD_CLR(clifd, &sock_state.rallfds);
                                                        FD_CLR(clifd, &sock_state.wallfds);
                                                        sockets[i].fd = -1;
       +                                                /* FIXME: what to do on error? */
                                                        close(clifd);
                                                        int newmaxsocket = -1;
                                                        for (int j = 0; j <= maxsocket; j++) {
       t@@ -518,18 +547,23 @@ ltk_mainloop(ltk_window *window) {
                                                                ltk_quit(window);
                                                                break;
                                                        }
       -                                        } else {
       -                                                process_commands(window, i);
                                                }
                                        }
       +                                /* FIXME: maybe ignore SIGPIPE signal - then don't call FD_CLR
       +                                   for wallfds above but rather when write fails with EPIPE */
       +                                /* -> this would possibly allow data to be written still in the
       +                                   hypothetical scenario that only the writing end of the socket
       +                                   is closed (and ltkd wouldn't crash if only the reading end is
       +                                   closed) */
                                        if (FD_ISSET(clifd, &wfds)) {
       +                                        /* FIXME: also call in loop like reading above */
                                                write_sock(&sockets[i]);
                                        }
                                }
                        }
        
                        clock_gettime(CLOCK_MONOTONIC, &now);
       -                ltk_timespecsub(&now, &last, &elapsed);
       +                ltk_timespecsub(&now, &lasttimer, &elapsed);
                        /* Note: it should be safe to give the same pointer as the first and
                           last argument, as long as ltk_timespecsub/add isn't changed incompatibly */
                        size_t i = 0;
       t@@ -539,7 +573,7 @@ ltk_mainloop(ltk_window *window) {
                                    (timers[i].remaining.tv_sec == 0 && timers[i].remaining.tv_nsec == 0)) {
                                        timers[i].callback(timers[i].data);
                                        if (timers[i].repeat.tv_sec == 0 && timers[i].repeat.tv_nsec == 0) {
       -                                        /* remove timers because it has no repeat */
       +                                        /* remove timer because it has no repeat */
                                                memmove(timers + i, timers + i + 1, sizeof(ltk_timer) * (timers_num - i - 1));
                                        } else {
                                                ltk_timespecadd(&timers[i].remaining, &timers[i].repeat, &timers[i].remaining);
       t@@ -549,13 +583,22 @@ ltk_mainloop(ltk_window *window) {
                                        i++;
                                }
                        }
       -                last = now;
       +                lasttimer = now;
        
                        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;
                        }
       +
       +                clock_gettime(CLOCK_MONOTONIC, &now);
       +                ltk_timespecsub(&now, &last, &elapsed);
       +                /* FIXME: configure framerate */
       +                if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
       +                        sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
       +                        nanosleep(&sleep_time, NULL);
       +                }
       +                last = now;
                }
        
                ltk_cleanup();
       t@@ -704,7 +747,7 @@ ltk_log_msg(const char *mode, const char *format, va_list args) {
        static int
        ltk_set_root_widget_cmd(
            ltk_window *window,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            int num_tokens,
            ltk_error *err) {
                ltk_widget *widget;
       t@@ -712,8 +755,12 @@ ltk_set_root_widget_cmd(
                        err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
                        err->arg = -1;
                        return 1;
       +        } else if (tokens[1].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 1;
       +                return 1;
                }
       -        widget = ltk_get_widget(tokens[1], LTK_WIDGET_ANY, err);
       +        widget = ltk_get_widget(tokens[1].text, LTK_WIDGET_ANY, err);
                if (!widget) {
                        err->arg = 1;
                        return 1;
       t@@ -1167,6 +1214,9 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
                int tw, th;
                ltk_text_line_get_size(tmp, &tw, &th);
                ltk_text_line_destroy(tmp);
       +        /* FIXME: cache doesn't really make any sense right now anyways
       +           since images are only loaded from memory */
       +        ltk_image_init(window->renderdata, 0);
        
                return window;
        }
       t@@ -1342,12 +1392,14 @@ push_token(struct token_list *tl, char *token) {
                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 = ltk_realloc(tl->tokens, new_size * sizeof(char *));
       +                ltk_cmd_token *new = ltk_reallocarray(tl->tokens, new_size, sizeof(ltk_cmd_token));
                        if (!new) return -1;
                        tl->tokens = new;
                        tl->num_alloc = new_size;
                }
       -        tl->tokens[tl->num_tokens++] = token;
       +        tl->tokens[tl->num_tokens].text = token;
       +        tl->tokens[tl->num_tokens].len = 0;
       +        tl->tokens[tl->num_tokens++].contains_nul = 0;
        
                return 0;
        }
       t@@ -1434,6 +1486,11 @@ accept_sock(int listenfd) {
                if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) {
                        return -1;
                }
       +        if (set_nonblock(clifd)) {
       +                /* FIXME: what could even be done if close fails? */
       +                close(clifd);
       +                return -1;
       +        }
        
                return clifd;
        }
       t@@ -1452,7 +1509,7 @@ read_sock(struct ltk_sock_info *sock) {
                   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);
       +                        sock->tokens.tokens[i].text = sock->read + (sock->tokens.tokens[i].text - old);
                        }
                }
                nread = read(sock->fd, sock->read + sock->read_len, READ_BLK_SIZE);
       t@@ -1467,7 +1524,7 @@ read_sock(struct ltk_sock_info *sock) {
           Returns -1 on error, 0 otherwise. */
        static int
        write_sock(struct ltk_sock_info *sock) {
       -        if (!sock->write_len)
       +        if (sock->write_len == sock->write_cur)
                        return 0;
                int write_len = WRITE_BLK_SIZE > sock->write_len - sock->write_cur ?
                                sock->write_len - sock->write_cur : WRITE_BLK_SIZE;
       t@@ -1495,6 +1552,7 @@ write_sock(struct ltk_sock_info *sock) {
        
        static void
        move_write_pos(struct ltk_sock_info *sock) {
       +        /* FIXME: also resize if too large */
                if (sock->write_cur > 0) {
                        memmove(sock->to_write, sock->to_write + sock->write_cur,
                                sock->write_len - sock->write_cur);
       t@@ -1564,12 +1622,17 @@ ltk_queue_sock_write_fmt(int client, const char *fmt, ...) {
        static int
        tokenize_command(struct ltk_sock_info *sock) {
                for (; sock->read_cur < sock->read_len; sock->read_cur++) {
       +                /* FIXME: strip extra whitespace? Or maybe just have a "hard" protocol where it always has to be one space. */
                        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++;
       +                        if (sock->bs / 2)
       +                                sock->offset++;
       +                        else
       +                                sock->tokens.tokens[sock->tokens.num_tokens - 1].len++;
                                sock->bs %= 2;
                                sock->read[sock->read_cur-sock->offset] = '\\';
                        } else if (sock->read[sock->read_cur] == '\n' && !sock->in_str) {
       t@@ -1577,6 +1640,7 @@ tokenize_command(struct ltk_sock_info *sock) {
                                sock->read_cur++;
                                sock->offset = 0;
                                sock->in_token = 0;
       +                        sock->bs = 0;
                                return 0;
                        } else if (sock->read[sock->read_cur] == '"') {
                                sock->offset++;
       t@@ -1589,8 +1653,13 @@ tokenize_command(struct ltk_sock_info *sock) {
                        } else if (sock->read[sock->read_cur] == ' ' && !sock->in_str) {
                                sock->read[sock->read_cur-sock->offset] = '\0';
                                sock->in_token = !sock->in_token;
       +                        sock->bs = 0;
                        } else {
                                sock->read[sock->read_cur-sock->offset] = sock->read[sock->read_cur];
       +                        /* FIXME: assert that num_tokens > 0 */
       +                        sock->tokens.tokens[sock->tokens.num_tokens - 1].len++;
       +                        if (sock->read[sock->read_cur] == '\0')
       +                                sock->tokens.tokens[sock->tokens.num_tokens - 1].contains_nul = 1;
                                sock->bs = 0;
                        }
                }
       t@@ -1602,42 +1671,61 @@ tokenize_command(struct ltk_sock_info *sock) {
        /* FIXME: this is really ugly and inefficient right now - it will be replaced with something
           more generic at some point (or maybe just with a binary protocol?) */
        static int
       -handle_mask_command(int client, char **tokens, size_t num_tokens, ltk_error *err) {
       +handle_mask_command(int client, ltk_cmd_token *tokens, size_t num_tokens, ltk_error *err) {
                if (num_tokens != 4 && num_tokens != 5) {
                        err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
                        err->arg = -1;
                        return 1;
                }
       +        /* FIXME: make this nicer */
       +        /* -> use generic cmd handling like the widgets */
       +        if (tokens[1].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 1;
       +                return 1;
       +        } else if (tokens[2].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 2;
       +                return 1;
       +        } else if (tokens[3].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 3;
       +                return 1;
       +        } else if (num_tokens == 5 && tokens[4].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 4;
       +                return 1;
       +        }
                uint32_t mask = 0;
                int lock = 0;
                int special = 0;
       -        ltk_widget *widget = ltk_get_widget(tokens[1], LTK_WIDGET_ANY, err);
       +        ltk_widget *widget = ltk_get_widget(tokens[1].text, LTK_WIDGET_ANY, err);
                if (!widget) {
                        err->arg = 1;
                        return 1;
                }
       -        if (!strcmp(tokens[2], "widget")) {
       -                if (!strcmp(tokens[3], "mousepress")) {
       +        if (!strcmp(tokens[2].text, "widget")) {
       +                if (!strcmp(tokens[3].text, "mousepress")) {
                                mask = LTK_PEVENTMASK_MOUSEPRESS;
       -                } else if (!strcmp(tokens[3], "mouserelease")) {
       +                } else if (!strcmp(tokens[3].text, "mouserelease")) {
                                mask = LTK_PEVENTMASK_MOUSERELEASE;
       -                } else if (!strcmp(tokens[3], "mousemotion")) {
       +                } else if (!strcmp(tokens[3].text, "mousemotion")) {
                                mask = LTK_PEVENTMASK_MOUSEMOTION;
       -                } else if (!strcmp(tokens[3], "configure")) {
       +                } else if (!strcmp(tokens[3].text, "configure")) {
                                mask = LTK_PEVENTMASK_CONFIGURE;
       -                } else if (!strcmp(tokens[3], "statechange")) {
       +                } else if (!strcmp(tokens[3].text, "statechange")) {
                                mask = LTK_PEVENTMASK_STATECHANGE;
       -                } else if (!strcmp(tokens[3], "none")) {
       +                } else if (!strcmp(tokens[3].text, "none")) {
                                mask = LTK_PEVENTMASK_NONE;
                        } else {
                                err->type = ERR_INVALID_ARGUMENT;
                                err->arg = 3;
                                return 1;
                        }
       -        } else if (!strcmp(tokens[2], "menuentry")) {
       -                if (!strcmp(tokens[3], "press")) {
       +        } else if (!strcmp(tokens[2].text, "menuentry")) {
       +                if (!strcmp(tokens[3].text, "press")) {
                                mask = LTK_PWEVENTMASK_MENUENTRY_PRESS;
       -                } else if (!strcmp(tokens[3], "none")) {
       +                } else if (!strcmp(tokens[3].text, "none")) {
                                mask = LTK_PWEVENTMASK_MENUENTRY_NONE;
                        } else {
                                err->type = ERR_INVALID_ARGUMENT;
       t@@ -1645,10 +1733,10 @@ handle_mask_command(int client, char **tokens, size_t num_tokens, ltk_error *err
                                return 1;
                        }
                        special = 1;
       -        } else if (!strcmp(tokens[2], "button")) {
       -                if (!strcmp(tokens[3], "press")) {
       +        } else if (!strcmp(tokens[2].text, "button")) {
       +                if (!strcmp(tokens[3].text, "press")) {
                                mask = LTK_PWEVENTMASK_BUTTON_PRESS;
       -                } else if (!strcmp(tokens[3], "none")) {
       +                } else if (!strcmp(tokens[3].text, "none")) {
                                mask = LTK_PWEVENTMASK_BUTTON_NONE;
                        } else {
                                err->type = ERR_INVALID_ARGUMENT;
       t@@ -1662,7 +1750,7 @@ handle_mask_command(int client, char **tokens, size_t num_tokens, ltk_error *err
                        return 1;
                }
                if (num_tokens == 5) {
       -                if (!strcmp(tokens[4], "lock")) {
       +                if (!strcmp(tokens[4].text, "lock")) {
                                lock = 1;
                        } else {
                                err->type = ERR_INVALID_ARGUMENT;
       t@@ -1671,7 +1759,7 @@ handle_mask_command(int client, char **tokens, size_t num_tokens, ltk_error *err
                        }
                }
        
       -        if (!strcmp(tokens[0], "mask-add")) {
       +        if (!strcmp(tokens[0].text, "mask-add")) {
                        if (lock) {
                                if (special)
                                        ltk_widget_add_to_event_lwmask(widget, client, mask);
       t@@ -1683,7 +1771,7 @@ handle_mask_command(int client, char **tokens, size_t num_tokens, ltk_error *err
                                else
                                        ltk_widget_add_to_event_mask(widget, client, mask);
                        }
       -        } else if (!strcmp(tokens[0], "mask-set")) {
       +        } else if (!strcmp(tokens[0].text, "mask-set")) {
                        if (lock) {
                                if (special)
                                        ltk_widget_set_event_lwmask(widget, client, mask);
       t@@ -1695,7 +1783,7 @@ handle_mask_command(int client, char **tokens, size_t num_tokens, ltk_error *err
                                else
                                        ltk_widget_set_event_mask(widget, client, mask);
                        }
       -        } else if (!strcmp(tokens[0], "mask-remove")) {
       +        } else if (!strcmp(tokens[0].text, "mask-remove")) {
                        if (lock) {
                                if (special)
                                        ltk_widget_remove_from_event_lwmask(widget, client, mask);
       t@@ -1723,7 +1811,7 @@ process_commands(ltk_window *window, int client) {
                if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
                        return 0;
                struct ltk_sock_info *sock = &sockets[client];
       -        char **tokens;
       +        ltk_cmd_token *tokens;
                int num_tokens;
                ltk_error errdetail = {ERR_NONE, -1};
                int err;
       t@@ -1731,7 +1819,9 @@ process_commands(ltk_window *window, int client) {
                int last = 0;
                uint32_t seq;
                const char *errstr;
       +        int contains_nul = 0;
                while (!tokenize_command(sock)) {
       +                contains_nul = 0;
                        err = 0;
                        tokens = sock->tokens.tokens;
                        num_tokens = sock->tokens.num_tokens;
       t@@ -1740,43 +1830,48 @@ process_commands(ltk_window *window, int client) {
                                errdetail.arg = -1;
                                err = 1;
                        } else {
       -                        seq = (uint32_t)ltk_strtonum(tokens[0], 0, UINT32_MAX, &errstr);
       +                        contains_nul = tokens[0].contains_nul;
       +                        seq = (uint32_t)ltk_strtonum(tokens[0].text, 0, UINT32_MAX, &errstr);
                                tokens++;
                                num_tokens--;
       -                        if (errstr) {
       +                        if (errstr || contains_nul) {
                                        errdetail.type = ERR_INVALID_SEQNUM;
                                        errdetail.arg = -1;
                                        err = 1;
                                        seq = sock->last_seq;
       -                        } else if (strcmp(tokens[0], "set-root-widget") == 0) {
       +                        } else if (tokens[0].contains_nul) {
       +                                errdetail.type = ERR_INVALID_ARGUMENT;
       +                                errdetail.arg = 0;
       +                                err = 1;
       +                                seq = sock->last_seq;
       +                        } else if (strcmp(tokens[0].text, "set-root-widget") == 0) {
                                        err = ltk_set_root_widget_cmd(window, tokens, num_tokens, &errdetail);
       -                        } else if (strcmp(tokens[0], "quit") == 0) {
       +                        } else if (strcmp(tokens[0].text, "quit") == 0) {
                                        ltk_quit(window);
                                        last = 1;
       -                        } else if (strcmp(tokens[0], "destroy") == 0) {
       +                        } else if (strcmp(tokens[0].text, "destroy") == 0) {
                                        err = ltk_widget_destroy_cmd(window, tokens, num_tokens, &errdetail);
       -                        } else if (strncmp(tokens[0], "mask", 4) == 0) {
       +                        } else if (strncmp(tokens[0].text, "mask", 4) == 0) {
                                        err = handle_mask_command(client, tokens, num_tokens, &errdetail);
       -                        } else if (strcmp(tokens[0], "event-unlock") == 0) {
       +                        } else if (strcmp(tokens[0].text, "event-unlock") == 0) {
                                        if (num_tokens != 2) {
                                                errdetail.type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
                                                errdetail.arg = -1;
                                                err = 1;
       -                                } else if (strcmp(tokens[1], "true") == 0) {
       +                                } else if (!tokens[1].contains_nul && strcmp(tokens[1].text, "true") == 0) {
                                                retval = 1;
       -                                } else if (strcmp(tokens[1], "false") == 0) {
       +                                } else if (!tokens[1].contains_nul && strcmp(tokens[1].text, "false") == 0) {
                                                retval = -1;
                                        } else {
                                                err = 1;
                                                errdetail.type = ERR_INVALID_ARGUMENT;
       -                                        errdetail.arg = -1;
                                                errdetail.arg = 1;
                                        }
                                        last = 1;
                                } else {
                                        int found = 0;
                                        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       -                                        if (widget_funcs[i].cmd && !strcmp(tokens[0], widget_funcs[i].name)) {
       +                                        if (widget_funcs[i].cmd && !strcmp(tokens[0].text, widget_funcs[i].name)) {
                                                        err = widget_funcs[i].cmd(window, tokens, num_tokens, &errdetail);
                                                        found = 1;
                                                }
       t@@ -1802,12 +1897,12 @@ process_commands(ltk_window *window, int client) {
                        if (last)
                                break;
                }
       -        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;
       +        if (sock->tokens.num_tokens > 0 && sock->tokens.tokens[0].text != sock->read) {
       +                memmove(sock->read, sock->tokens.tokens[0].text, sock->read + sock->read_len - sock->tokens.tokens[0].text);
       +                ptrdiff_t offset = sock->tokens.tokens[0].text - sock->read;
                        /* Hmm, seems a bit ugly... */
                        for (int i = 0; i < sock->tokens.num_tokens; i++) {
       -                        sock->tokens.tokens[i] -= offset;
       +                        sock->tokens.tokens[i].text -= offset;
                        }
                        sock->read_len -= offset;
                        sock->read_cur -= offset;
   DIR diff --git a/src/macros.h b/src/macros.h
       t@@ -2,6 +2,8 @@
        #define _MACROS_H_
        
        /* stolen from OpenBSD */
       +/* Note: some code calls these macros with the same first and last
       +   argument, so it is important that that doesn't cause bad behavior. */
        #define ltk_timespecadd(tsp, usp, vsp)                                  \
                do {                                                            \
                        (vsp)->tv_sec = (tsp)->tv_sec + (usp)->tv_sec;          \
   DIR diff --git a/src/menu.c b/src/menu.c
       t@@ -1346,7 +1346,7 @@ static int
        ltk_menu_cmd_create(
            ltk_window *window,
            ltk_menu *menu_unneeded,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                (void)menu_unneeded;
       t@@ -1377,7 +1377,7 @@ static int
        ltk_menu_cmd_insert_entry(
            ltk_window *window,
            ltk_menu *menu,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -1398,7 +1398,7 @@ static int
        ltk_menu_cmd_add_entry(
            ltk_window *window,
            ltk_menu *menu,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -1418,7 +1418,7 @@ static int
        ltk_menu_cmd_remove_entry_index(
            ltk_window *window,
            ltk_menu *menu,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -1438,7 +1438,7 @@ static int
        ltk_menu_cmd_remove_entry_id(
            ltk_window *window,
            ltk_menu *menu,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -1458,7 +1458,7 @@ static int
        ltk_menu_cmd_remove_all_entries(
            ltk_window *window,
            ltk_menu *menu,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                (void)window;
       t@@ -1474,7 +1474,7 @@ static int
        ltk_menuentry_cmd_create(
            ltk_window *window,
            ltk_menuentry *e_unneeded,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                (void)e_unneeded;
       t@@ -1501,7 +1501,7 @@ static int
        ltk_menuentry_cmd_attach_submenu(
            ltk_window *window,
            ltk_menuentry *e,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                ltk_cmdarg_parseinfo cmd[] = {
       t@@ -1523,7 +1523,7 @@ static int
        ltk_menuentry_cmd_detach_submenu(
            ltk_window *window,
            ltk_menuentry *e,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                (void)window;
       t@@ -1539,7 +1539,7 @@ ltk_menuentry_cmd_detach_submenu(
        
        static struct menu_cmd {
                char *name;
       -        int (*func)(ltk_window *, ltk_menu *, char **, size_t, ltk_error *);
       +        int (*func)(ltk_window *, ltk_menu *, ltk_cmd_token *, size_t, ltk_error *);
                int needs_all;
        } menu_cmds[] = {
                {"add-entry", &ltk_menu_cmd_add_entry, 0},
       t@@ -1552,7 +1552,7 @@ static struct menu_cmd {
        
        static struct menuentry_cmd {
                char *name;
       -        int (*func)(ltk_window *, ltk_menuentry *, char **, size_t, ltk_error *);
       +        int (*func)(ltk_window *, ltk_menuentry *, ltk_cmd_token *, size_t, ltk_error *);
                int needs_all;
        } menuentry_cmds[] = {
                {"attach-submenu", &ltk_menuentry_cmd_attach_submenu, 0},
   DIR diff --git a/src/menu.h b/src/menu.h
       t@@ -17,6 +17,7 @@
        #ifndef LTK_MENU_H
        #define LTK_MENU_H
        
       +#include "cmd.h"
        #include "ltk.h"
        #include "text.h"
        #include "widget.h"
       t@@ -69,18 +70,7 @@ int ltk_submenuentry_ini_handler(ltk_window *window, const char *prop, const cha
        int ltk_submenuentry_fill_theme_defaults(ltk_window *window);
        void ltk_submenuentry_uninitialize_theme(ltk_window *window);
        
       -int ltk_menu_cmd(
       -        ltk_window *window,
       -        char **tokens,
       -        size_t num_tokens,
       -        ltk_error *err
       -);
       -
       -int ltk_menuentry_cmd(
       -        ltk_window *window,
       -        char **tokens,
       -        size_t num_tokens,
       -        ltk_error *err
       -);
       +GEN_CMD_HELPERS_PROTO(ltk_menu_cmd)
       +GEN_CMD_HELPERS_PROTO(ltk_menuentry_cmd)
        
        #endif /* LTK_MENU_H */
   DIR diff --git a/src/proto_types.h b/src/proto_types.h
       t@@ -1,16 +1,17 @@
        #ifndef LTK_PROTO_TYPES_H
        #define LTK_PROTO_TYPES_H
        
       -#define LTK_WIDGET_UNKNOWN   0
       -#define LTK_WIDGET_ANY       1
       -#define LTK_WIDGET_GRID      2
       -#define LTK_WIDGET_BUTTON    3
       -#define LTK_WIDGET_LABEL     4
       -#define LTK_WIDGET_BOX       5
       -#define LTK_WIDGET_MENU      6
       -#define LTK_WIDGET_MENUENTRY 7
       -#define LTK_WIDGET_ENTRY     8
       -#define LTK_NUM_WIDGETS      9
       +#define LTK_WIDGET_UNKNOWN       0
       +#define LTK_WIDGET_ANY           1
       +#define LTK_WIDGET_GRID          2
       +#define LTK_WIDGET_BUTTON        3
       +#define LTK_WIDGET_LABEL         4
       +#define LTK_WIDGET_BOX           5
       +#define LTK_WIDGET_MENU          6
       +#define LTK_WIDGET_MENUENTRY     7
       +#define LTK_WIDGET_ENTRY         8
       +#define LTK_WIDGET_IMAGE         9
       +#define LTK_NUM_WIDGETS          10
        
        #define LTK_WIDGETMASK_UNKNOWN    (UINT32_C(1) << LTK_WIDGET_UNKNOWN)
        #define LTK_WIDGETMASK_ANY        (UINT32_C(0xFFFF))
       t@@ -21,6 +22,7 @@
        #define LTK_WIDGETMASK_MENU       (UINT32_C(1) << LTK_WIDGET_MENU)
        #define LTK_WIDGETMASK_MENUENTRY  (UINT32_C(1) << LTK_WIDGET_MENUENTRY)
        #define LTK_WIDGETMASK_ENTRY      (UINT32_C(1) << LTK_WIDGET_ENTRY)
       +#define LTK_WIDGETMASK_IMAGE      (UINT32_C(1) << LTK_WIDGET_IMAGE)
        
        /* P == protocol; W == widget */
        
   DIR diff --git a/src/text_pango.c b/src/text_pango.c
       t@@ -101,6 +101,7 @@ ltk_text_line_create(ltk_text_context *ctx, uint16_t font_size, char *text, int 
                        tl->text = ltk_strdup(text);
                tl->len = strlen(tl->text);
                tl->font_size = font_size;
       +        /* FIXME: does this ever return NULL? */
                tl->layout = pango_layout_new(ctx->context);
        
                PangoFontDescription *desc = pango_font_description_from_string(ctx->default_font);
   DIR diff --git a/src/util.c b/src/util.c
       t@@ -15,6 +15,7 @@
         */
        
        #include <pwd.h>
       +#include <fcntl.h>
        #include <ctype.h>
        #include <errno.h>
        #include <stdio.h>
       t@@ -409,3 +410,13 @@ next_utf8(char *text, size_t len, size_t index) {
                        i++;
                return i;
        }
       +
       +int
       +set_nonblock(int fd) {
       +        int flags = fcntl(fd, F_GETFL, 0);
       +        if (flags == -1)
       +                return -1;
       +        if (fcntl(fd, F_SETFL, flags | O_NONBLOCK))
       +                return -1;
       +        return 0;
       +}
   DIR diff --git a/src/util.h b/src/util.h
       t@@ -53,6 +53,8 @@ int str_array_equal(const char *terminated, const char *array, size_t len);
        size_t prev_utf8(char *text, size_t index);
        size_t next_utf8(char *text, size_t len, size_t index);
        
       +int set_nonblock(int fd);
       +
        #define LENGTH(X) (sizeof(X) / sizeof(X[0]))
        
        #endif /* _LTK_UTIL_H_ */
   DIR diff --git a/src/widget.c b/src/widget.c
       t@@ -1301,7 +1301,7 @@ ltk_widget_destroy(ltk_widget *widget, int shallow, ltk_error *err) {
        int
        ltk_widget_destroy_cmd(
            ltk_window *window,
       -    char **tokens,
       +    ltk_cmd_token *tokens,
            size_t num_tokens,
            ltk_error *err) {
                (void)window;
       t@@ -1311,10 +1311,19 @@ ltk_widget_destroy_cmd(
                        err->arg = -1;
                        return 1;
                }
       +        if (tokens[1].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 1;
       +                return 1;
       +        } else if (num_tokens == 3 && tokens[2].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 2;
       +                return 1;
       +        }
                if (num_tokens == 3) {
       -                if (strcmp(tokens[2], "deep") == 0) {
       +                if (strcmp(tokens[2].text, "deep") == 0) {
                                shallow = 0;
       -                } else if (strcmp(tokens[2], "shallow") == 0) {
       +                } else if (strcmp(tokens[2].text, "shallow") == 0) {
                                shallow = 1;
                        } else {
                                err->type = ERR_INVALID_ARGUMENT;
       t@@ -1322,7 +1331,7 @@ ltk_widget_destroy_cmd(
                                return 1;
                        }
                }
       -        ltk_widget *widget = ltk_get_widget(tokens[1], LTK_WIDGET_ANY, err);
       +        ltk_widget *widget = ltk_get_widget(tokens[1].text, LTK_WIDGET_ANY, err);
                if (!widget) {
                        err->arg = 1;
                        return 1;
   DIR diff --git a/src/widget.h b/src/widget.h
       t@@ -1,5 +1,5 @@
        /*
       - * Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org>
       + * Copyright (c) 2021-2023 lumidify <nobody@lumidify.org>
         *
         * Permission to use, copy, modify, and/or distribute this software for any
         * purpose with or without fee is hereby granted, provided that the above
       t@@ -44,7 +44,10 @@ typedef enum {
                LTK_STICKY_LEFT = 1 << 0,
                LTK_STICKY_RIGHT = 1 << 1,
                LTK_STICKY_TOP = 1 << 2,
       -        LTK_STICKY_BOTTOM = 1 << 3
       +        LTK_STICKY_BOTTOM = 1 << 3,
       +        LTK_STICKY_SHRINK_WIDTH = 1 << 4,
       +        LTK_STICKY_SHRINK_HEIGHT = 1 << 5,
       +        LTK_STICKY_PRESERVE_ASPECT_RATIO = 1 << 6,
        } ltk_sticky_mask;
        
        typedef enum {
       t@@ -164,7 +167,7 @@ struct ltk_widget_vtable {
        
        void ltk_widget_hide(ltk_widget *widget);
        int ltk_widget_destroy(ltk_widget *widget, int shallow, ltk_error *err);
       -int ltk_widget_destroy_cmd(struct ltk_window *window, char **tokens, size_t num_tokens, ltk_error *err);
       +int ltk_widget_destroy_cmd(struct ltk_window *window, ltk_cmd_token *tokens, size_t num_tokens, ltk_error *err);
        void ltk_fill_widget_defaults(ltk_widget *widget, const char *id, struct ltk_window *window,
            struct ltk_widget_vtable *vtable, int w, int h);
        void ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state);
   DIR diff --git a/test.gui b/test.gui
       t@@ -5,23 +5,23 @@ grid grd1 set-column-weight 0 1
        grid grd1 set-column-weight 1 1
        set-root-widget grd1
        box box1 create vertical
       -grid grd1 add box1 0 0 1 1 nsew
       +grid grd1 add box1 0 0 1 1 lrtb
        button btn1 create "I'm a button!"
        button btn2 create "I'm also a button!"
        button btn3 create "I'm another boring button."
       -box box1 add btn1 ew
       -box box1 add btn2 e
       +box box1 add btn1 lr
       +box box1 add btn2 r
        box box1 add btn3
        box box2 create vertical
       -grid grd1 add box2 1 0 1 1 nsew
       +grid grd1 add box2 1 0 1 1 lrtb
        button btn4 create "2 I'm a button!"
        button btn5 create "2 I'm also a button!"
        button btn6 create "2 I'm another boring button."
       -box box2 add btn4 ew
       -box box2 add btn5 e
       +box box2 add btn4 lr
       +box box2 add btn5 r
        box box2 add btn6
        button btn7 create "Button 7"
        button btn8 create "Button 8"
       -grid grd1 add btn7 0 1 1 1 nsew
       -grid grd1 add btn8 1 1 1 1 ew
       +grid grd1 add btn7 0 1 1 1 lrtb
       +grid grd1 add btn8 1 1 1 1 lr
        mask-add btn1 button press
   DIR diff --git a/test2.gui b/test2.gui
       t@@ -41,5 +41,5 @@ menuentry entry14 attach-submenu submenu2
        submenu submenu3 create
        menu submenu3 add-entry entry16
        menuentry entry15 attach-submenu submenu3
       -grid grd1 add menu1 0 0 1 1 ew
       +grid grd1 add menu1 0 0 1 1 lr
        mask-add entry10 menuentry press
   DIR diff --git a/test3.gui b/test3.gui
       t@@ -13,4 +13,4 @@ grid grd1 add btn2 1 0 1 1
        grid grd1 add btn3 2 0 1 1
        mask-add btn1 button press
        entry entry1 create "Hi"
       -grid grd1 add entry1 3 0 1 1 ew
       +grid grd1 add entry1 3 0 1 1 w
   DIR diff --git a/test3.sh b/test3.sh
       t@@ -14,7 +14,7 @@ do
                        echo "quit"
                        ;;
                *)
       -                printf "%s\n" "$cmd" >&2
       +                printf "client1: %s\n" "$cmd" >&2
                        ;;
                esac
        done | ./src/ltkc $ltk_id
   DIR diff --git a/testimg.sh b/testimg.sh
       t@@ -0,0 +1,13 @@
       +#!/bin/sh
       +
       +export LTKDIR="`pwd`/.ltk"
       +ltk_id=`./src/ltkd -t "Cool Window"`
       +#if [ $? -ne 0 ]; then
       +#        echo "Unable to start ltkd." >&2
       +#        exit 1
       +#fi
       +
       +{ printf "grid grd1 create 2 1\ngrid grd1 set-row-weight 0 1\ngrid grd1 set-row-weight 1 1\ngrid grd1 set-column-weight 0 1\nset-root-widget grd1\nimage img1 create test.png \""; ./src/ltkc_img < ~/test.png; printf "\"\ngrid grd1 add img1 1 0 1 1 lrp\n"; } |./src/ltkc $ltk_id | while read cmd
       +do
       +        printf "%s\n" "$cmd" >&2
       +done | ./src/ltkc $ltk_id