URI: 
       Add basic combobox; improve external command handling - ltk - GUI toolkit for X11 (WIP)
  HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/ltk.git (encrypted, but very slow)
  HTML git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ltk.git (over tor)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 5a0a7594e75569bc17cc48a0ec0e5975ec51d54d
   DIR parent d0faf9b6f4464428cac5de55e56cd1a0a92b45ef
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Mon,  6 May 2024 23:33:09 +0200
       
       Add basic combobox; improve external command handling
       
       The combobox is very hacky and doesn't behave properly
       in all circumstances.
       
       Diffstat:
         M Makefile                            |       2 ++
         M config.example/ltk.cfg              |      30 +++++++++++++++++++++++++++++-
         M examples/ltk/test.c                 |      12 ++++++++++--
         A src/ltk/combobox.c                  |     596 +++++++++++++++++++++++++++++++
         A src/ltk/combobox.h                  |      43 ++++++++++++++++++++++++++++++
         M src/ltk/config.c                    |     265 ++++++++++++++++++++++++++++++-
         M src/ltk/config.h                    |      19 ++++++++++++++++++-
         M src/ltk/entry.c                     |       3 ++-
         M src/ltk/event_xlib.c                |       1 +
         M src/ltk/eventdefs.h                 |       1 +
         M src/ltk/ltk.c                       |     236 ++++++++++++++++++++-----------
         M src/ltk/ltk.h                       |       6 ------
         M src/ltk/memory.h                    |       2 --
         M src/ltk/menu.c                      |      55 +++++++++++++++++++++----------
         M src/ltk/menu.h                      |       6 +++++-
         M src/ltk/text.h                      |       2 ++
         M src/ltk/text_pango.c                |      10 ++++++++++
         M src/ltk/txtbuf.c                    |      10 ++++++++++
         M src/ltk/txtbuf.h                    |      13 +++++++++++++
         M src/ltk/util.c                      |     188 -------------------------------
         M src/ltk/widget.h                    |       2 ++
         M src/ltk/widget_internal.h           |      13 +++++++++++++
         M src/ltk/window.c                    |      17 +++++++----------
       
       23 files changed, 1214 insertions(+), 318 deletions(-)
       ---
   DIR diff --git a/Makefile b/Makefile
       @@ -59,6 +59,7 @@ OBJ_LTK = \
                src/ltk/button.o \
                src/ltk/checkbutton.o \
                src/ltk/radiobutton.o \
       +        src/ltk/combobox.o \
                src/ltk/graphics_xlib.o \
                src/ltk/surface_cache.o \
                src/ltk/event_xlib.o \
       @@ -98,6 +99,7 @@ HDR_LTK = \
                src/ltk/button.h \
                src/ltk/checkbutton.h \
                src/ltk/radiobutton.h \
       +        src/ltk/combobox.h \
                src/ltk/color.h \
                src/ltk/label.h \
                src/ltk/rect.h \
   DIR diff --git a/config.example/ltk.cfg b/config.example/ltk.cfg
       @@ -1,7 +1,28 @@
        [general]
        explicit-focus = true
        all-activatable = true
       +
       +# FIXME: document weird parsing for commands (quotes, backslashes)
       +# FIXME: actually test all of these options...
       +# Options for commands:
       +# %f: combined input/output file
       +# %i: input file
       +# %o: output file
       +# If %i is specified but %o is not specified,
       +# output is read from stdout (and vice versa).
       +# If %f is specified, %i and %o are not allowed.
       +# If no files are specified, input is written to
       +# stdin and output is read from stdout.
       +
       +# line-editor is given the contents of a line entry
       +# and must return the edited text. Newlines are
       +# stripped from the returning text.
        line-editor = "st -e vi %f"
       +# option-chooser is given several options, one on
       +# each line, and must return one of them. If the
       +# result contains newlines, only the part before
       +# the first newline is used.
       +option-chooser = dmenu
        mixed-dpi = true
        fixed-dpi = 96
        dpi-scale = 1.0
       @@ -45,7 +66,11 @@ fg-disabled = "#292929"
        # bind edit-text-external ...
        # bind edit-line-external ...
        bind-keypress move-next sym tab
       -bind-keypress move-prev sym tab mods shift
       +# FIXME: how should this be handled? it's a bit weird because
       +# shift+tab causes left tab + shift under X11, but that
       +# requires shift to be given here, so maybe that should be
       +# abstracted away in the backend?
       +bind-keypress move-prev sym left-tab mods shift
        bind-keypress move-next text n
        bind-keypress move-prev text p
        bind-keypress move-left sym left
       @@ -81,6 +106,9 @@ bind-keypress paste-clipboard text v mods ctrl
        bind-keypress switch-selection-side text o mods alt
        bind-keypress edit-external text E mods ctrl
        
       +[key-binding:combobox]
       +bind-keypress choose-external text E mods ctrl
       +
        # default mapping (just to silence warnings)
        [key-mapping]
        language = "English (US)"
   DIR diff --git a/examples/ltk/test.c b/examples/ltk/test.c
       @@ -11,6 +11,7 @@
        #include <ltk/box.h>
        #include <ltk/checkbutton.h>
        #include <ltk/radiobutton.h>
       +#include <ltk/combobox.h>
        
        int
        quit(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       @@ -93,7 +94,14 @@ main(int argc, char *argv[]) {
                ltk_box_add(box, LTK_CAST_WIDGET(rbtn1), LTK_STICKY_LEFT);
                ltk_box_add(box, LTK_CAST_WIDGET(rbtn2), LTK_STICKY_LEFT);
        
       -        ltk_grid_add(grid, LTK_CAST_WIDGET(menu), 0, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
       +        ltk_combobox *combo = ltk_combobox_create(window);
       +        ltk_combobox_add_option(combo, "Option 1");
       +        ltk_combobox_add_option(combo, "Option 2");
       +        ltk_combobox_add_option(combo, "Option 3");
       +        ltk_combobox_add_option(combo, "Option 4");
       +
       +        ltk_grid_add(grid, LTK_CAST_WIDGET(menu), 0, 0, 1, 1, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
       +        ltk_grid_add(grid, LTK_CAST_WIDGET(combo), 0, 1, 1, 1, LTK_STICKY_LEFT);
                ltk_grid_add(grid, LTK_CAST_WIDGET(button), 1, 0, 1, 1, LTK_STICKY_LEFT);
                ltk_grid_add(grid, LTK_CAST_WIDGET(button1), 1, 1, 1, 1, LTK_STICKY_RIGHT);
                ltk_grid_add(grid, LTK_CAST_WIDGET(label), 2, 0, 1, 1, LTK_STICKY_RIGHT);
       @@ -102,7 +110,7 @@ main(int argc, char *argv[]) {
                ltk_grid_add(grid, LTK_CAST_WIDGET(box), 4, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT|LTK_STICKY_TOP|LTK_STICKY_BOTTOM);
                ltk_window_set_root_widget(window, LTK_CAST_WIDGET(grid));
                ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
       -        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(e4), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
       +        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(e4), LTK_MENUENTRY_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
                ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button1), LTK_BUTTON_SIGNAL_PRESSED, &printstuff, LTK_MAKE_ARG_INT(5));
                ltk_widget_register_signal_handler(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, &quit, LTK_ARG_VOID);
                ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button1), LTK_WIDGET_SIGNAL_CHANGE_STATE, &printstate, LTK_ARG_VOID);
   DIR diff --git a/src/ltk/combobox.c b/src/ltk/combobox.c
       @@ -0,0 +1,596 @@
       +/*
       + * Copyright (c) 2024 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 "config.h"
       +#include "combobox.h"
       +#include "color.h"
       +#include "graphics.h"
       +#include "ltk.h"
       +#include "memory.h"
       +#include "rect.h"
       +#include "text.h"
       +#include "util.h"
       +#include "widget.h"
       +#include "menu.h"
       +#include "widget_internal.h"
       +
       +#define MAX_COMBOBOX_BORDER_WIDTH 10000
       +#define MAX_COMBOBOX_PADDING 50000
       +#define MAX_COMBOBOX_ARROW_SIZE 50000
       +
       +static void ltk_combobox_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
       +static int ltk_combobox_release(ltk_widget *self);
       +static void ltk_combobox_destroy(ltk_widget *self, int shallow);
       +static void ltk_combobox_recalc_ideal_size(ltk_widget *self);
       +static int ltk_combobox_remove_child(ltk_widget *self, ltk_widget *widget);
       +static ltk_widget *ltk_combobox_get_child(ltk_widget *self);
       +static ltk_widget *ltk_combobox_nearest_child(ltk_widget *self, ltk_rect rect);
       +static int ltk_combobox_key_press(ltk_widget *self, ltk_key_event *event);
       +static int choose_external(ltk_widget *self, ltk_key_event *event);
       +static void ltk_combobox_cmd_return(ltk_widget *self, char *text, size_t len);
       +
       +static struct ltk_widget_vtable vtable = {
       +        .key_press = &ltk_combobox_key_press,
       +        .key_release = NULL,
       +        .mouse_press = NULL,
       +        .mouse_release = NULL,
       +        .release = &ltk_combobox_release,
       +        .motion_notify = NULL,
       +        .mouse_leave = NULL,
       +        .mouse_enter = NULL,
       +        .change_state = NULL,
       +        .get_child_at_pos = NULL,
       +        .cmd_return = &ltk_combobox_cmd_return,
       +        .resize = NULL,
       +        .hide = NULL,
       +        .draw = &ltk_combobox_draw,
       +        .destroy = &ltk_combobox_destroy,
       +        .child_size_change = NULL,
       +        .remove_child = &ltk_combobox_remove_child,
       +        .first_child = &ltk_combobox_get_child,
       +        .last_child = &ltk_combobox_get_child,
       +        .nearest_child = &ltk_combobox_nearest_child,
       +        .recalc_ideal_size = &ltk_combobox_recalc_ideal_size,
       +        .type = LTK_WIDGET_COMBOBOX,
       +        .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS,
       +        .invalid_signal = LTK_COMBOBOX_SIGNAL_INVALID,
       +};
       +
       +static struct {
       +        ltk_color *border;
       +        ltk_color *border_pressed;
       +        ltk_color *border_hover;
       +        ltk_color *border_active;
       +        ltk_color *border_disabled;
       +        ltk_color *fill;
       +        ltk_color *fill_pressed;
       +        ltk_color *fill_hover;
       +        ltk_color *fill_active;
       +        ltk_color *fill_disabled;
       +        ltk_color *text;
       +        ltk_color *text_pressed;
       +        ltk_color *text_hover;
       +        ltk_color *text_active;
       +        ltk_color *text_disabled;
       +
       +        char *font;
       +        ltk_size arrow_size;
       +        ltk_size border_width;
       +        ltk_size pad;
       +        ltk_size font_size;
       +        int compress_borders;
       +} theme;
       +
       +static ltk_theme_parseinfo parseinfo[] = {
       +        {"border", THEME_COLOR, {.color = &theme.border}, {.color = "#339999"}, 0, 0, 0},
       +        {"border-hover", THEME_COLOR, {.color = &theme.border_hover}, {.color = "#FFFFFF"}, 0, 0, 0},
       +        {"border-active", THEME_COLOR, {.color = &theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
       +        {"border-disabled", THEME_COLOR, {.color = &theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
       +        {"border-pressed", THEME_COLOR, {.color = &theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
       +        {"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#113355"}, 0, 0, 0},
       +        {"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#738194"}, 0, 0, 0},
       +        {"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#113355"}, 0, 0, 0},
       +        {"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
       +        {"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
       +        {"text", THEME_COLOR, {.color = &theme.text}, {.color = "#FFFFFF"}, 0, 0, 0},
       +        {"text-hover", THEME_COLOR, {.color = &theme.text_hover}, {.color = "#FFFFFF"}, 0, 0, 0},
       +        {"text-active", THEME_COLOR, {.color = &theme.text_active}, {.color = "#FFFFFF"}, 0, 0, 0},
       +        {"text-disabled", THEME_COLOR, {.color = &theme.text_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
       +        {"text-pressed", THEME_COLOR, {.color = &theme.text_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
       +
       +        {"arrow-size", THEME_SIZE, {.size = &theme.arrow_size}, {.size = {.val = 250, .unit = LTK_UNIT_MM}}, 0, MAX_COMBOBOX_ARROW_SIZE, 0},
       +        {"border-width", THEME_SIZE, {.size = &theme.border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_COMBOBOX_BORDER_WIDTH, 0},
       +        {"pad", THEME_SIZE, {.size = &theme.pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_COMBOBOX_PADDING, 0},
       +        {"compress-borders", THEME_BOOL, {.b = &theme.compress_borders}, {.b = 0}, 0, MAX_COMBOBOX_PADDING, 0},
       +        {"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0},
       +        {"font-size", THEME_SIZE, {.size = &theme.font_size}, {.size = {.val = 1200, .unit = LTK_UNIT_PT}}, 0, 20000, 0},
       +};
       +
       +void
       +ltk_combobox_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
       +        *p = parseinfo;
       +        *len = LENGTH(parseinfo);
       +}
       +
       +static ltk_keybinding_cb cb_map[] = {
       +        {"choose-external", &choose_external},
       +};
       +
       +static ltk_array(keypress) *keypresses = NULL;
       +
       +void
       +ltk_combobox_get_keybinding_parseinfo(
       +        ltk_keybinding_cb **press_cbs_ret, size_t *press_len_ret,
       +        ltk_keybinding_cb **release_cbs_ret, size_t *release_len_ret,
       +        ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret
       +) {
       +        *press_cbs_ret = cb_map;
       +        *press_len_ret = LENGTH(cb_map);
       +        *release_cbs_ret = NULL;
       +        *release_len_ret = 0;
       +        if (!keypresses)
       +                keypresses = ltk_array_create(keypress, 1);
       +        *presses_ret = keypresses;
       +        *releases_ret = NULL;
       +}
       +
       +void
       +ltk_combobox_cleanup(void) {
       +        ltk_keypress_bindings_destroy(keypresses);
       +        keypresses = NULL;
       +}
       +
       +/* FIXME: a lot more theme settings */
       +static void
       +ltk_combobox_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
       +        ltk_combobox *combobox = LTK_CAST_COMBOBOX(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;
       +
       +        int arrow_size = ltk_size_to_pixel(theme.arrow_size, self->last_dpi);
       +        int pad = ltk_size_to_pixel(theme.pad, self->last_dpi);
       +        int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi);
       +        ltk_color *border = NULL, *fill = NULL, *text = NULL;
       +        if (self->state & LTK_DISABLED) {
       +                border = theme.border_disabled;
       +                fill = theme.fill_disabled;
       +                text = theme.text_disabled;
       +        } else if (self->state & LTK_PRESSED) {
       +                border = theme.border_pressed;
       +                fill = theme.fill_pressed;
       +                text = theme.text_pressed;
       +        } else if (self->state & LTK_HOVER) {
       +                border = theme.border_hover;
       +                fill = theme.fill_hover;
       +                text = theme.text_hover;
       +        } else if (self->state & LTK_ACTIVE) {
       +                border = theme.border_active;
       +                fill = theme.fill_active;
       +                text = theme.text_active;
       +        } else {
       +                border = theme.border;
       +                fill = theme.fill;
       +                text = theme.text;
       +        }
       +        ltk_rect draw_rect = {x, y, lrect.w, lrect.h};
       +        ltk_rect draw_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h};
       +        ltk_surface_fill_rect(draw_surf, fill, draw_clip);
       +        if (bw > 0) {
       +                ltk_surface_draw_border_clipped(
       +                        draw_surf, border, draw_rect, bw, LTK_BORDER_ALL, draw_clip
       +                );
       +        }
       +        int text_w, text_h;
       +        ltk_text_line_get_size(combobox->tl, &text_w, &text_h);
       +        int text_x = x + pad;
       +        int text_y = y + (lrect.h - text_h) / 2;
       +        ltk_text_line_draw_clipped(combobox->tl, draw_surf, text, text_x, text_y, draw_clip);
       +
       +        ltk_point arrow_points[] = {
       +            {x + lrect.w - pad - bw - arrow_size, y + lrect.h / 2 - arrow_size / 2},
       +            {x + lrect.w - pad - bw, y + lrect.h / 2 - arrow_size / 2},
       +            {x + lrect.w - pad - bw - arrow_size / 2, y + lrect.h / 2 + arrow_size / 2}
       +        };
       +        ltk_surface_fill_polygon_clipped(draw_surf, text, arrow_points, LENGTH(arrow_points), draw_clip);
       +        self->dirty = 0;
       +}
       +
       +/* FIXME: this is kind of ugly because it uses a lot of internal knowledge about menus */
       +static void
       +popup_dropdown(ltk_combobox *combobox) {
       +        if (!combobox->dropdown || ltk_menu_get_num_entries(combobox->dropdown) == 0)
       +                return;
       +        ltk_rect combo_rect = LTK_CAST_WIDGET(combobox)->lrect;
       +        ltk_point combo_global = ltk_widget_pos_to_global(LTK_CAST_WIDGET(combobox), 0, 0);
       +
       +        int win_w = LTK_CAST_WIDGET(combobox)->window->rect.w;
       +        int win_h = LTK_CAST_WIDGET(combobox)->window->rect.h;
       +        ltk_menu *dropdown = combobox->dropdown;
       +        ltk_widget_recalc_ideal_size(LTK_CAST_WIDGET(dropdown));
       +        int ideal_w = dropdown->widget.ideal_w;
       +        int ideal_h = dropdown->widget.ideal_h;
       +        int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h;
       +        int combo_bw = ltk_size_to_pixel(theme.border_width, LTK_CAST_WIDGET(combobox)->last_dpi);
       +
       +        int space_top = combo_global.y;
       +        int space_bottom = win_h - (combo_global.y + combo_rect.h);
       +        int y_top = combo_global.y - ideal_h;
       +        int y_bottom = combo_global.y + combo_rect.h;
       +        if (theme.compress_borders) {
       +                y_top += combo_bw;
       +                y_bottom -= combo_bw;
       +        }
       +        if (space_top > space_bottom) {
       +                y_final = y_top;
       +                if (y_final < 0) {
       +                        y_final = 0;
       +                        h_final = combo_rect.y;
       +                }
       +        } else {
       +                y_final = y_bottom;
       +                if (space_bottom < ideal_h)
       +                        h_final = space_bottom;
       +        }
       +        /* FIXME: maybe threshold so there's always at least a part of
       +           the menu contents shown (instead of maybe just a few pixels) */
       +        /* pathological case where window is way too small */
       +        if (h_final <= 0) {
       +                y_final = 0;
       +                h_final = win_h;
       +        }
       +        x_final = combo_global.x;
       +        if (x_final + ideal_w > win_w)
       +                x_final = win_w - ideal_w;
       +        if (x_final < 0) {
       +                x_final = 0;
       +                w_final = win_w;
       +        }
       +
       +        /* reset everything just in case */
       +        dropdown->x_scroll_offset = dropdown->y_scroll_offset = 0;
       +        dropdown->scroll_top_hover = dropdown->scroll_bottom_hover = 0;
       +        dropdown->scroll_left_hover = dropdown->scroll_right_hover = 0;
       +        dropdown->widget.lrect.x = x_final;
       +        dropdown->widget.lrect.y = y_final;
       +        dropdown->widget.lrect.w = w_final;
       +        dropdown->widget.lrect.h = h_final;
       +        dropdown->widget.crect = LTK_CAST_WIDGET(dropdown)->lrect;
       +        dropdown->widget.dirty = 1;
       +        dropdown->widget.hidden = 0;
       +        dropdown->popup_submenus = 0;
       +        dropdown->unpopup_submenus_on_hide = 1;
       +        ltk_widget_resize(LTK_CAST_WIDGET(dropdown));
       +        ltk_window_register_popup(LTK_CAST_WIDGET(combobox)->window, LTK_CAST_WIDGET(dropdown));
       +        ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(dropdown)->window, LTK_CAST_WIDGET(dropdown));
       +}
       +
       +static void
       +unpopup_dropdown(ltk_combobox *combobox) {
       +        if (combobox->dropdown && !LTK_CAST_WIDGET(combobox->dropdown)->hidden) {
       +                ltk_widget_hide(LTK_CAST_WIDGET(combobox->dropdown));
       +        }
       +}
       +
       +/* FIXME: set ideal width to ideal width of submenu */
       +/* FIXME: disable button when no options */
       +
       +static int
       +ltk_combobox_release(ltk_widget *self) {
       +        ltk_combobox *combo = LTK_CAST_COMBOBOX(self);
       +        if (!combo->dropdown)
       +                return 0;
       +        if (combo->dropdown->widget.hidden)
       +                popup_dropdown(combo);
       +        else
       +                unpopup_dropdown(combo);
       +        return 1;
       +}
       +
       +#define MAX(a, b) ((a) > (b) ? (a) : (b))
       +
       +static void
       +recalc_ideal_size(ltk_combobox *combobox) {
       +        int text_w, text_h;
       +        ltk_text_line_get_size(combobox->tl, &text_w, &text_h);
       +        int arrow_size = ltk_size_to_pixel(theme.arrow_size, LTK_CAST_WIDGET(combobox)->last_dpi);
       +        int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(combobox)->last_dpi);
       +        combobox->widget.ideal_w = text_w + pad * 3 + arrow_size;
       +        combobox->widget.ideal_h = MAX(text_h, arrow_size) + pad * 2;
       +}
       +
       +static void
       +ltk_combobox_recalc_ideal_size(ltk_widget *self) {
       +        ltk_combobox *combobox = LTK_CAST_COMBOBOX(self);
       +        int font_size = ltk_size_to_pixel(theme.font_size, self->last_dpi);
       +        ltk_text_line_set_font_size(combobox->tl, font_size);
       +        recalc_ideal_size(combobox);
       +}
       +
       +static void
       +combobox_set_active(ltk_combobox *combo, size_t idx, const char *text) {
       +        combo->cur_active = idx;
       +        ltk_text_line_set_const_text(combo->tl, text);
       +        recalc_ideal_size(combo);
       +        if (combo->widget.parent && combo->widget.parent->vtable->child_size_change) {
       +                combo->widget.parent->vtable->child_size_change(combo->widget.parent, LTK_CAST_WIDGET(combo));
       +        }
       +        ltk_widget_emit_signal(LTK_CAST_WIDGET(combo), LTK_COMBOBOX_SIGNAL_CHANGED, LTK_EMPTY_ARGLIST);
       +}
       +
       +static int
       +handle_entry_pressed(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       +        (void)args;
       +        ltk_menuentry *e = LTK_CAST_MENUENTRY(self);
       +        ltk_combobox *combo = LTK_CAST_COMBOBOX(LTK_CAST_ARG_WIDGET(data));
       +        if (!combo->dropdown) /* shouldn't be possible */
       +                return 1;
       +        size_t idx = ltk_menu_get_entry_index(combo->dropdown, e);
       +        if (idx == SIZE_MAX) /* shouldn't be possible */
       +                return 1;
       +        combobox_set_active(combo, idx, ltk_menuentry_get_text(e));
       +        return 1;
       +}
       +
       +static void
       +ltk_combobox_cmd_return(ltk_widget *self, char *text, size_t len) {
       +        ltk_combobox *combo = LTK_CAST_COMBOBOX(self);
       +        if (!combo->dropdown)
       +                return;
       +        /* need to copy since it's not nul-terminated */
       +        char *textcopy = ltk_strndup(text, len);
       +        char *nl = strchr(textcopy, '\n');
       +        /* only take text until first newline into account */
       +        if (nl)
       +                *nl = '\0';
       +        for (size_t i = 0; i < ltk_menu_get_num_entries(combo->dropdown); i++) {
       +                if (!strcmp(textcopy, ltk_menuentry_get_text(ltk_menu_get_entry(combo->dropdown, i)))) {
       +                        combobox_set_active(combo, i, textcopy);
       +                        break;
       +                }
       +        }
       +        ltk_free(textcopy);
       +}
       +
       +static int
       +choose_external(ltk_widget *self, ltk_key_event *event) {
       +        (void)event;
       +        ltk_combobox *combo = LTK_CAST_COMBOBOX(self);
       +        if (!combo->dropdown || ltk_menu_get_num_entries(combo->dropdown) == 0)
       +                return 0;
       +        ltk_general_config *config = ltk_config_get_general();
       +        /* FIXME: allow arguments to key mappings - this would allow to have different key mappings
       +           for different editors instead of just one command */
       +        if (!config->option_chooser) {
       +                ltk_warn("Unable to run external option choosing command: option chooser not configured.");
       +        } else {
       +                /* FIXME: somehow show that there was an error if this returns 1? */
       +                /* FIXME: change interface to not require length of cmd */
       +                txtbuf *tmpbuf = txtbuf_new();
       +                for (size_t i = 0; i < ltk_menu_get_num_entries(combo->dropdown); i++) {
       +                        txtbuf_append(tmpbuf, ltk_menuentry_get_text(ltk_menu_get_entry(combo->dropdown, i)));
       +                        txtbuf_append(tmpbuf, "\n");
       +                }
       +                ltk_call_cmd(self, config->option_chooser, txtbuf_get_text(tmpbuf), txtbuf_len(tmpbuf));
       +                txtbuf_destroy(tmpbuf);
       +        }
       +        return 0;
       +}
       +
       +static int
       +ltk_combobox_key_press(ltk_widget *self, ltk_key_event *event) {
       +        ltk_keypress_binding b;
       +        for (size_t i = 0; i < ltk_array_len(keypresses); i++) {
       +                b = ltk_array_get(keypresses, i).b;
       +                if ((b.mods == event->modmask && b.sym != LTK_KEY_NONE && b.sym == event->sym) ||
       +                    (b.mods == (event->modmask & ~LTK_MOD_SHIFT) &&
       +                     ((b.text && event->mapped && !strcmp(b.text, event->mapped)) ||
       +                      (b.rawtext && event->text && !strcmp(b.rawtext, event->text))))) {
       +                        ltk_array_get(keypresses, i).cb.func(self, event);
       +                        self->dirty = 1;
       +                        ltk_window_invalidate_widget_rect(self->window, self);
       +                        return 1;
       +                }
       +        }
       +        return 0;
       +}
       +
       +const char *
       +ltk_combobox_get_text(ltk_combobox *combo) {
       +        if (!combo->dropdown)
       +                return NULL;
       +        ltk_menuentry *e = ltk_menu_get_entry(combo->dropdown, combo->cur_active);
       +        if (!e)
       +                return NULL;
       +        return ltk_menuentry_get_text(e);
       +}
       +
       +size_t
       +ltk_combobox_get_index(ltk_combobox *combo) {
       +        return combo->cur_active;
       +}
       +
       +/* FIXME: this is really hacky - it was added to remove some weird effects when moving
       +   around with keyboard shortcuts */
       +/* FIXME: movement is still weird, for instance when pressing left on the dropdown,
       +   focus moves to the combobox, not to the widget to the left - maybe there needs to be
       +   another widget flag so the combobox is activatable but isn't taken into account when
       +   moving back up from the child to the parent */
       +/* FIXME: maybe just have a dedicated dropdown instead of reusing a menu in order to fix
       +   these weirdnesses? */
       +static int
       +handle_dropdown_change_state(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       +        (void)args;
       +        ltk_menu *menu = LTK_CAST_MENU(self);
       +        ltk_combobox *combo = LTK_CAST_COMBOBOX(LTK_CAST_ARG_WIDGET(data));
       +        if (menu != combo->dropdown) /* should never happen */
       +                return 0;
       +        if (!(menu->widget.state & LTK_ACTIVE) && !menu->widget.hidden)
       +                ltk_widget_hide(self);
       +        return 0;
       +}
       +
       +int
       +ltk_combobox_insert_option(ltk_combobox *combobox, const char *option, size_t idx) {
       +        unpopup_dropdown(combobox); /* just to avoid weird effects */
       +        if (!combobox->dropdown) {
       +                combobox->dropdown = ltk_submenu_create(LTK_CAST_WIDGET(combobox)->window);
       +                LTK_CAST_WIDGET(combobox->dropdown)->parent = LTK_CAST_WIDGET(combobox);
       +                ltk_widget_register_signal_handler(
       +                        LTK_CAST_WIDGET(combobox->dropdown), LTK_WIDGET_SIGNAL_CHANGE_STATE,
       +                        &handle_dropdown_change_state, LTK_MAKE_ARG_WIDGET(LTK_CAST_WIDGET(combobox))
       +                );
       +        }
       +        ltk_menuentry *e = ltk_menuentry_create(LTK_CAST_WIDGET(combobox)->window, option);
       +        if (ltk_menu_insert_entry(combobox->dropdown, e, idx)) {
       +                ltk_widget_destroy(LTK_CAST_WIDGET(e), 0);
       +                return 1;
       +        }
       +        size_t num = ltk_menu_get_num_entries(combobox->dropdown);
       +        if (num == 1) {
       +                combobox_set_active(combobox, 0, option);
       +        } else if (idx <= combobox->cur_active && combobox->cur_active < num) {
       +                combobox->cur_active++;
       +        }
       +        ltk_widget_register_signal_handler(
       +                LTK_CAST_WIDGET(e), LTK_MENUENTRY_SIGNAL_PRESSED,
       +                &handle_entry_pressed, LTK_MAKE_ARG_WIDGET(LTK_CAST_WIDGET(combobox))
       +        );
       +        return 0;
       +}
       +
       +int
       +ltk_combobox_add_option(ltk_combobox *combobox, const char *option) {
       +        /* it's easier to just completely ban options with newlines instead of
       +           dealing with weird cases where the external option-chooser splits
       +           options at newlines */
       +        /* FIXME: should any other chars be banned? */
       +        if (strchr(option, '\n'))
       +                return 1;
       +        unpopup_dropdown(combobox); /* just to avoid weird effects */
       +        if (!combobox->dropdown) {
       +                combobox->dropdown = ltk_submenu_create(LTK_CAST_WIDGET(combobox)->window);
       +                LTK_CAST_WIDGET(combobox->dropdown)->parent = LTK_CAST_WIDGET(combobox);
       +                ltk_widget_register_signal_handler(
       +                        LTK_CAST_WIDGET(combobox->dropdown), LTK_WIDGET_SIGNAL_CHANGE_STATE,
       +                        &handle_dropdown_change_state, LTK_MAKE_ARG_WIDGET(LTK_CAST_WIDGET(combobox))
       +                );
       +        }
       +        ltk_menuentry *e = ltk_menuentry_create(LTK_CAST_WIDGET(combobox)->window, option);
       +        /* this should never fail */
       +        ltk_menu_add_entry(combobox->dropdown, e);
       +        size_t num = ltk_menu_get_num_entries(combobox->dropdown);
       +        if (num == 1) {
       +                combobox_set_active(combobox, 0, option);
       +        }
       +        ltk_widget_register_signal_handler(
       +                LTK_CAST_WIDGET(e), LTK_MENUENTRY_SIGNAL_PRESSED,
       +                &handle_entry_pressed, LTK_MAKE_ARG_WIDGET(LTK_CAST_WIDGET(combobox))
       +        );
       +        return 0;
       +}
       +
       +int
       +ltk_combobox_remove_option_index(ltk_combobox *combobox, size_t idx) {
       +        if (!combobox->dropdown)
       +                return 1;
       +        unpopup_dropdown(combobox); /* just to avoid weird effects */
       +        ltk_menuentry *e = ltk_menu_remove_entry_index(combobox->dropdown, idx);
       +        if (!e) return 1;
       +        ltk_widget_destroy(LTK_CAST_WIDGET(e), 0);
       +        if (idx == combobox->cur_active) {
       +                size_t num = ltk_menu_get_num_entries(combobox->dropdown);
       +                if (num == 0) {
       +                        combobox_set_active(combobox, SIZE_MAX, "");
       +                } else {
       +                        e = ltk_menu_get_entry(combobox->dropdown, combobox->cur_active);
       +                        if (!e) ltk_fatal("Unable to get menu entry. This should not happen.");
       +                        combobox_set_active(combobox, idx >= num ? num - 1 : idx, ltk_menuentry_get_text(e));
       +                }
       +        }
       +        return 0;
       +}
       +
       +void
       +ltk_combobox_remove_all_options(ltk_combobox *combobox) {
       +        if (!combobox->dropdown)
       +                return;
       +        unpopup_dropdown(combobox); /* just to avoid weird effects */
       +        ltk_menu_remove_all_entries(combobox->dropdown);
       +        combobox_set_active(combobox, SIZE_MAX, "");
       +}
       +
       +/* NOTE: This should never be called since the dropdown is managed
       +   completely by the combobox, but it's here just in case. */
       +static int
       +ltk_combobox_remove_child(ltk_widget *self, ltk_widget *widget) {
       +        ltk_combobox *combo = LTK_CAST_COMBOBOX(self);
       +        if (widget != LTK_CAST_WIDGET(combo->dropdown))
       +                return 1;
       +        widget->parent = NULL;
       +        combo->dropdown = NULL;
       +        return 0;
       +}
       +
       +static ltk_widget *
       +ltk_combobox_get_child(ltk_widget *self) {
       +        ltk_combobox *combo = LTK_CAST_COMBOBOX(self);
       +        if (combo->dropdown && !combo->dropdown->widget.hidden)
       +                return LTK_CAST_WIDGET(combo->dropdown);
       +        return NULL;
       +}
       +
       +static ltk_widget *
       +ltk_combobox_nearest_child(ltk_widget *self, ltk_rect rect) {
       +        (void)rect;
       +        return ltk_combobox_get_child(self);
       +}
       +
       +ltk_combobox *
       +ltk_combobox_create(ltk_window *window) {
       +        ltk_combobox *combobox = ltk_malloc(sizeof(ltk_combobox));
       +        ltk_fill_widget_defaults(LTK_CAST_WIDGET(combobox), window, &vtable, 0, 0);
       +        combobox->dropdown = NULL;
       +        combobox->cur_active = SIZE_MAX;
       +
       +        /* FIXME: only create once text has been added */
       +        combobox->tl = ltk_text_line_create_const_text_default(
       +                theme.font, ltk_size_to_pixel(theme.font_size, combobox->widget.last_dpi), "", -1
       +        );
       +        recalc_ideal_size(combobox);
       +        combobox->widget.dirty = 1;
       +
       +        return combobox;
       +}
       +
       +static void
       +ltk_combobox_destroy(ltk_widget *self, int shallow) {
       +        (void)shallow;
       +        ltk_combobox *combo = LTK_CAST_COMBOBOX(self);
       +        if (!combo) {
       +                ltk_warn("Tried to destroy NULL combobox.\n");
       +                return;
       +        }
       +        ltk_text_line_destroy(combo->tl);
       +        if (combo->dropdown) {
       +                LTK_CAST_WIDGET(combo->dropdown)->parent = NULL;
       +                ltk_widget_destroy(LTK_CAST_WIDGET(combo->dropdown), 0);
       +        }
       +        ltk_free(combo);
       +}
   DIR diff --git a/src/ltk/combobox.h b/src/ltk/combobox.h
       @@ -0,0 +1,43 @@
       +/*
       + * Copyright (c) 2024 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_COMBOBOX_H
       +#define LTK_COMBOBOX_H
       +
       +#include "text.h"
       +#include "widget.h"
       +#include "window.h"
       +#include "menu.h"
       +
       +#define LTK_COMBOBOX_SIGNAL_CHANGED -1
       +#define LTK_COMBOBOX_SIGNAL_INVALID -2
       +
       +typedef struct {
       +        ltk_widget widget;
       +        ltk_text_line *tl;
       +        ltk_menu *dropdown;
       +        size_t cur_active;
       +} ltk_combobox;
       +
       +ltk_combobox *ltk_combobox_create(ltk_window *window);
       +int ltk_combobox_insert_option(ltk_combobox *combobox, const char *option, size_t idx);
       +int ltk_combobox_add_option(ltk_combobox *combobox, const char *option);
       +int ltk_combobox_remove_option_index(ltk_combobox *combobox, size_t idx);
       +void ltk_combobox_remove_all_options(ltk_combobox *combobox);
       +const char *ltk_combobox_get_text(ltk_combobox *combo);
       +size_t ltk_combobox_get_index(ltk_combobox *combo);
       +
       +#endif /* LTK_COMBOBOX_H */
   DIR diff --git a/src/ltk/config.c b/src/ltk/config.c
       @@ -35,8 +35,11 @@ static ltk_general_config general_config;
        static ltk_language_mapping *mappings = NULL;
        static size_t mappings_alloc = 0, mappings_len = 0;
        
       +static ltk_array(cmd) *ltk_parse_cmd(const char *cmdtext, size_t len);
       +
        static ltk_theme_parseinfo general_parseinfo[] = {
       -        {"line-editor", THEME_STRING, {.str = &general_config.line_editor}, {.str = NULL}, 0, 0, 0},
       +        {"line-editor", THEME_CMD, {.cmd = &general_config.line_editor}, {.cmd = NULL}, 0, 0, 0},
       +        {"option-chooser", THEME_CMD, {.cmd = &general_config.option_chooser}, {.cmd = NULL}, 0, 0, 0},
                {"dpi-scale", THEME_DOUBLE, {.d = &general_config.dpi_scale}, {.d = 1.0}, 10, 10000, 0},
                {"explicit-focus", THEME_BOOL, {.b = &general_config.explicit_focus}, {.b = 0}, 0, 0, 0},
                {"all-activatable", THEME_BOOL, {.b = &general_config.all_activatable}, {.b = 0}, 0, 0, 0},
       @@ -64,6 +67,7 @@ static struct {
                        ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret
                );
        } keybinding_handlers[] = {
       +        {"combobox", &ltk_combobox_get_keybinding_parseinfo},
                {"entry", &ltk_entry_get_keybinding_parseinfo},
                {"window", &ltk_window_get_keybinding_parseinfo},
        };
       @@ -86,6 +90,7 @@ static struct theme_handlerinfo {
                {"theme:submenuentry", &ltk_submenuentry_get_theme_parseinfo, "theme:window", 0},
                {"theme:checkbutton", &ltk_checkbutton_get_theme_parseinfo, "theme:window", 0},
                {"theme:radiobutton", &ltk_radiobutton_get_theme_parseinfo, "theme:window", 0},
       +        {"theme:combobox", &ltk_combobox_get_theme_parseinfo, "theme:window", 0},
        };
        
        GEN_SORT_SEARCH_HELPERS(themehandler, struct theme_handlerinfo, name)
       @@ -101,6 +106,39 @@ sort_themehandlers(void) {
                }
        }
        
       +LTK_ARRAY_INIT_FUNC_DECL_STATIC(cmdpiece, struct ltk_cmd_piece)
       +LTK_ARRAY_INIT_IMPL_STATIC(cmdpiece, struct ltk_cmd_piece)
       +LTK_ARRAY_INIT_FUNC_DECL_STATIC(cmd, ltk_array(cmdpiece) *)
       +LTK_ARRAY_INIT_IMPL_STATIC(cmd, ltk_array(cmdpiece) *)
       +
       +static void
       +cmd_piece_free_helper(struct ltk_cmd_piece p) {
       +        if (p.text)
       +                ltk_free(p.text);
       +}
       +
       +static void
       +cmd_free_helper(ltk_array(cmdpiece) *arr) {
       +        ltk_array_destroy_deep(cmdpiece, arr, &cmd_piece_free_helper);
       +}
       +
       +static ltk_array(cmd) *
       +copy_cmd(ltk_array(cmd) *cmd) {
       +        ltk_array(cmd) *cmdcopy = ltk_array_create(cmd, ltk_array_len(cmd));
       +        for (size_t i = 0; i < ltk_array_len(cmd); i++) {
       +                ltk_array(cmdpiece) *piece = ltk_array_get(cmd, i);
       +                ltk_array(cmdpiece) *piececopy = ltk_array_create(cmdpiece, ltk_array_len(piece));
       +                for (size_t j = 0; j < ltk_array_len(piece); j++) {
       +                        struct ltk_cmd_piece p = {NULL, ltk_array_get(piece, j).type};
       +                        if (ltk_array_get(piece, j).text)
       +                                p.text = ltk_strdup(ltk_array_get(piece, j).text);
       +                        ltk_array_append(cmdpiece, piececopy, p);
       +                }
       +                ltk_array_append(cmd, cmdcopy, piececopy);
       +        }
       +        return cmdcopy;
       +}
       +
        /* FIXME: handle '#' or no '#' in color specification */
        static int
        handle_theme_setting(ltk_renderdata *renderdata, ltk_theme_parseinfo *entry, const char *value) {
       @@ -170,6 +208,11 @@ handle_theme_setting(ltk_renderdata *renderdata, ltk_theme_parseinfo *entry, con
                                return 1;
                        entry->initialized = 1;
                        break;
       +        case THEME_CMD:
       +                if (!(*(entry->ptr.cmd) = ltk_parse_cmd(value, strlen(value))))
       +                        return 1;
       +                entry->initialized = 1;
       +                break;
                case THEME_BOOL:
                        if (strcmp(value, "true") == 0) {
                                *(entry->ptr.b) = 1;
       @@ -266,11 +309,25 @@ fill_single_theme_defaults(ltk_renderdata *renderdata, struct theme_handlerinfo 
                                if (ep) {
                                        if (!(*(e->ptr.color) = ltk_color_copy(renderdata, *(ep->ptr.color))))
                                                return 1;
       +                        } else if (!e->defaultval.color) {
       +                                return 1; /* colors must always be initialized */
                                } else if (!(*(e->ptr.color) = ltk_color_create(renderdata, e->defaultval.color))) {
                                        return 1;
                                }
                                e->initialized = 1;
                                break;
       +                case THEME_CMD:
       +                        if (ep) {
       +                                /* There is no reason to ever use this, but whatever */
       +                                if (!(*(e->ptr.cmd) = copy_cmd(*(ep->ptr.cmd))))
       +                                        return 1;
       +                        } else if (!e->defaultval.cmd) {
       +                                *(e->ptr.cmd) = NULL;
       +                        } else if (!(*(e->ptr.cmd) = ltk_parse_cmd(e->defaultval.cmd, strlen(e->defaultval.cmd)))) {
       +                                return 1;
       +                        }
       +                        e->initialized = 1;
       +                        break;
                        case THEME_BOOL:
                                *(e->ptr.b) = ep ? *(ep->ptr.b) : e->defaultval.b;
                                e->initialized = 1;
       @@ -316,6 +373,10 @@ uninitialize_theme(ltk_renderdata *renderdata) {
                                        ltk_color_destroy(renderdata, *(e->ptr.color));
                                        e->initialized = 0;
                                        break;
       +                        case THEME_CMD:
       +                                ltk_array_destroy_deep(cmd, *(e->ptr.cmd), &cmd_free_helper);
       +                                e->initialized = 0;
       +                                break;
                                case THEME_SIZE:
                                case THEME_INT:
                                case THEME_UINT:
       @@ -878,7 +939,7 @@ ltk_config_get_language_mapping(size_t idx) {
                return &mappings[idx];
        }
        
       -int
       +static int
        str_array_prefix(const char *str, const char *ar, size_t len) {
                size_t slen = strlen(str);
                if (len < slen)
       @@ -1086,7 +1147,7 @@ ltk_config_parsefile(ltk_renderdata *renderdata, const char *filename, char **er
        }
        
        /* FIXME: update this */
       -const char *default_config = "[general]\n"
       +static const char *default_config = "[general]\n"
        "explicit-focus = true\n"
        "all-activatable = true\n"
        "[key-binding:window]\n"
       @@ -1179,6 +1240,7 @@ static struct keysym_mapping {
                {"kp-up", LTK_KEY_KP_UP},
        
                {"left", LTK_KEY_LEFT},
       +        {"left-tab", LTK_KEY_LEFT_TAB},
                {"linefeed", LTK_KEY_LINEFEED},
                {"menu", LTK_KEY_MENU},
                {"mode-switch", LTK_KEY_MODE_SWITCH},
       @@ -1218,3 +1280,200 @@ parse_keysym(char *keysym_str, size_t len, ltk_keysym *sym) {
                *sym = km->keysym;
                return 0;
        }
       +
       +/* FIXME: this is really ugly */
       +/* FIXME: this handles double-quote, but the config parser already uses that, so
       +   it's kind of weird because it's parsed twice (also backslashes are parsed twice). */
       +static ltk_array(cmd) *
       +ltk_parse_cmd(const char *cmdtext, size_t len) {
       +        int bs = 0;
       +        int in_sqstr = 0;
       +        int in_dqstr = 0;
       +        int in_ws = 1;
       +        int inout_used = 0, input_used = 0, output_used = 0;
       +        char c;
       +        size_t cur_start = 0;
       +        int offset = 0;
       +        ltk_array(cmdpiece) *cur_arg = ltk_array_create(cmdpiece, 1);
       +        ltk_array(cmd) *cmd = ltk_array_create(cmd, 4);
       +        char *cmdcopy = ltk_strndup(cmdtext, len);
       +        for (size_t i = 0; i < len; i++) {
       +                c = cmdcopy[i];
       +                if (c == '\\') {
       +                        if (bs) {
       +                                offset++;
       +                                bs = 0;
       +                        } else {
       +                                bs = 1;
       +                        }
       +                } else if (isspace(c)) {
       +                        if (!in_sqstr && !in_dqstr) {
       +                                if (bs) {
       +                                        if (in_ws) {
       +                                                in_ws = 0;
       +                                                cur_start = i;
       +                                                offset = 0;
       +                                        } else {
       +                                                offset++;
       +                                        }
       +                                        bs = 0;
       +                                } else if (!in_ws) {
       +                                        /* FIXME: shouldn't this be < instead of <=? */
       +                                        if (cur_start <= i - offset) {
       +                                                struct ltk_cmd_piece p = {ltk_strndup(cmdcopy + cur_start, i - cur_start - offset), LTK_CMD_TEXT};
       +                                                ltk_array_append(cmdpiece, cur_arg, p);
       +                                        }
       +                                        /* FIXME: cmd is named horribly */
       +                                        ltk_array_append(cmd, cmd, cur_arg);
       +                                        cur_arg = ltk_array_create(cmdpiece, 1);
       +                                        in_ws = 1;
       +                                        offset = 0;
       +                                }
       +                        /* FIXME: parsing weird here - bs just ignored */
       +                        } else if (bs) {
       +                                bs = 0;
       +                        }
       +                } else if (c == '%') {
       +                        if (bs) {
       +                                if (in_ws) {
       +                                        cur_start = i;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                }
       +                                bs = 0;
       +                        } else if (!in_sqstr && i < len - 1 && (cmdcopy[i + 1] == 'f' || cmdcopy[i + 1] == 'i' || cmdcopy[i + 1] == 'o')) {
       +                                if (!in_ws && cur_start < i - offset) {
       +                                        struct ltk_cmd_piece p = {ltk_strndup(cmdcopy + cur_start, i - cur_start - offset), LTK_CMD_TEXT};
       +                                        ltk_array_append(cmdpiece, cur_arg, p);
       +                                }
       +                                struct ltk_cmd_piece p = {NULL, LTK_CMD_INOUT_FILE};
       +                                switch (cmdcopy[i + 1]) {
       +                                case 'f':
       +                                        p.type = LTK_CMD_INOUT_FILE;
       +                                        if (input_used || output_used)
       +                                                goto error;
       +                                        inout_used = 1;
       +                                        break;
       +                                case 'i':
       +                                        p.type = LTK_CMD_INPUT_FILE;
       +                                        if (inout_used)
       +                                                goto error;
       +                                        input_used = 1;
       +                                        break;
       +                                case 'o':
       +                                        p.type = LTK_CMD_OUTPUT_FILE;
       +                                        if (inout_used)
       +                                                goto error;
       +                                        output_used = 1;
       +                                        break;
       +                                default:
       +                                        ltk_fatal("Impossible.");
       +                                }
       +                                ltk_array_append(cmdpiece, cur_arg, p);
       +                                i++;
       +                                cur_start = i + 1;
       +                                offset = 0;
       +                        } else if (in_ws) {
       +                                cur_start = i;
       +                                offset = 0;
       +                        }
       +                        in_ws = 0;
       +                } else if (c == '"') {
       +                        if (in_sqstr) {
       +                                bs = 0;
       +                        } else if (bs) {
       +                                if (in_ws) {
       +                                        cur_start = i;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                }
       +                                bs = 0;
       +                        } else if (in_dqstr) {
       +                                offset++;
       +                                in_dqstr = 0;
       +                                continue;
       +                        } else {
       +                                in_dqstr = 1;
       +                                if (in_ws) {
       +                                        cur_start = i + 1;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                        continue;
       +                                }
       +                        }
       +                        in_ws = 0;
       +                } else if (c == '\'') {
       +                        if (in_dqstr) {
       +                                bs = 0;
       +                        } else if (bs) {
       +                                if (in_ws) {
       +                                        cur_start = i;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                }
       +                                bs = 0;
       +                        } else if (in_sqstr) {
       +                                offset++;
       +                                in_sqstr = 0;
       +                                continue;
       +                        } else {
       +                                in_sqstr = 1;
       +                                if (in_ws) {
       +                                        cur_start = i + 1;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                        continue;
       +                                }
       +                        }
       +                        in_ws = 0;
       +                } else if (bs) {
       +                        if (!in_sqstr && !in_dqstr) {
       +                                if (in_ws) {
       +                                        cur_start = i;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                }
       +                        }
       +                        bs = 0;
       +                        in_ws = 0;
       +                } else {
       +                        if (in_ws) {
       +                                cur_start = i;
       +                                offset = 0;
       +                        }
       +                        in_ws = 0;
       +                }
       +                cmdcopy[i - offset] = cmdcopy[i];
       +        }
       +        /* FIXME: proper error messages with errstr */
       +        if (in_sqstr || in_dqstr) {
       +                /*ltk_warn("Unterminated string in command\n");*/
       +                goto error;
       +        }
       +        if (!in_ws) {
       +                if (cur_start <= len - offset) {
       +                        struct ltk_cmd_piece p = {ltk_strndup(cmdcopy + cur_start, len - cur_start - offset), LTK_CMD_TEXT};
       +                        ltk_array_append(cmdpiece, cur_arg, p);
       +                }
       +                ltk_array_append(cmd, cmd, cur_arg);
       +                cur_arg = NULL;
       +        }
       +        if (cmd->len == 0) {
       +                /*ltk_warn("Empty command\n");*/
       +                goto error;
       +        }
       +        ltk_free(cmdcopy);
       +        return cmd;
       +error:
       +        ltk_free(cmdcopy);
       +        if (cur_arg)
       +                ltk_array_destroy_deep(cmdpiece, cur_arg, &cmd_piece_free_helper);
       +        ltk_array_destroy_deep(cmd, cmd, &cmd_free_helper);
       +        return NULL;
       +}
   DIR diff --git a/src/ltk/config.h b/src/ltk/config.h
       @@ -55,8 +55,22 @@ typedef struct {
                size_t mappings_alloc, mappings_len;
        } ltk_language_mapping;
        
       +struct ltk_cmd_piece {
       +        char *text;
       +        enum {
       +                LTK_CMD_TEXT,
       +                LTK_CMD_INPUT_FILE,
       +                LTK_CMD_OUTPUT_FILE,
       +                LTK_CMD_INOUT_FILE,
       +        } type;
       +};
       +
       +LTK_ARRAY_INIT_STRUCT_DECL(cmdpiece, struct ltk_cmd_piece)
       +LTK_ARRAY_INIT_STRUCT_DECL(cmd, ltk_array(cmdpiece) *)
       +
        typedef struct {
       -        char *line_editor;
       +        ltk_array(cmd) *line_editor;
       +        ltk_array(cmd) *option_chooser;
                double dpi_scale;
                double fixed_dpi;
                int mixed_dpi;
       @@ -90,6 +104,7 @@ typedef enum {
                THEME_BORDERSIDES,
                THEME_SIZE,
                THEME_DOUBLE,
       +        THEME_CMD,
        } ltk_theme_datatype;
        
        typedef struct {
       @@ -106,6 +121,7 @@ typedef struct {
                        ltk_border_sides *border;
                        ltk_size *size;
                        double *d;
       +                ltk_array(cmd) **cmd;
                } ptr;
                /* Note: The default color is also given as a string
                   because it has to be allocated first (it is only a
       @@ -120,6 +136,7 @@ typedef struct {
                        ltk_border_sides border;
                        ltk_size size;
                        double d;
       +                char *cmd;
                } defaultval;
                /* FIXME: min/max doesn't make too much sense for sizes since they
                   can use different units, but that shouldn't matter for now because
   DIR diff --git a/src/ltk/entry.c b/src/ltk/entry.c
       @@ -39,6 +39,7 @@
        #include "util.h"
        #include "widget.h"
        #include "config.h"
       +#include "widget_internal.h"
        
        #define MAX_ENTRY_BORDER_WIDTH 10000
        #define MAX_ENTRY_PADDING 50000
       @@ -577,7 +578,7 @@ edit_external(ltk_widget *self, ltk_key_event *event) {
                } else {
                        /* FIXME: somehow show that there was an error if this returns 1? */
                        /* FIXME: change interface to not require length of cmd */
       -                ltk_call_cmd(LTK_CAST_WIDGET(entry), config->line_editor, strlen(config->line_editor), entry->text, entry->len);
       +                ltk_call_cmd(LTK_CAST_WIDGET(entry), config->line_editor, entry->text, entry->len);
                }
                return 0;
        }
   DIR diff --git a/src/ltk/event_xlib.c b/src/ltk/event_xlib.c
       @@ -622,6 +622,7 @@ static struct keysym_mapping {
                {XK_space, LTK_KEY_SPACE},
                {XK_Sys_Req, LTK_KEY_SYS_REQ},
                {XK_Tab, LTK_KEY_TAB},
       +        {XK_ISO_Left_Tab, LTK_KEY_LEFT_TAB},
                {XK_Up, LTK_KEY_UP},
                {XK_Undo, LTK_KEY_UNDO},
        };
   DIR diff --git a/src/ltk/eventdefs.h b/src/ltk/eventdefs.h
       @@ -140,6 +140,7 @@ typedef enum {
                LTK_KEY_SPACE,
                LTK_KEY_SYS_REQ,
                LTK_KEY_TAB,
       +        LTK_KEY_LEFT_TAB,
                LTK_KEY_UP,
                LTK_KEY_UNDO
        } ltk_keysym;
   DIR diff --git a/src/ltk/ltk.c b/src/ltk/ltk.c
       @@ -45,8 +45,9 @@
        #include "widget_internal.h"
        
        typedef struct {
       -        char *tmpfile;
                ltk_widget *caller;
       +        char *infile;
       +        char *outfile;
                int pid;
        } ltk_cmdinfo;
        
       @@ -54,8 +55,8 @@ LTK_ARRAY_INIT_DECL_STATIC(window, ltk_window *)
        LTK_ARRAY_INIT_IMPL_STATIC(window, ltk_window *)
        LTK_ARRAY_INIT_DECL_STATIC(rwindow, ltk_renderwindow *)
        LTK_ARRAY_INIT_IMPL_STATIC(rwindow, ltk_renderwindow *)
       -LTK_ARRAY_INIT_DECL_STATIC(cmd, ltk_cmdinfo)
       -LTK_ARRAY_INIT_IMPL_STATIC(cmd, ltk_cmdinfo)
       +LTK_ARRAY_INIT_DECL_STATIC(cmdinfo, ltk_cmdinfo)
       +LTK_ARRAY_INIT_IMPL_STATIC(cmdinfo, ltk_cmdinfo)
        
        static struct {
                ltk_renderdata *renderdata;
       @@ -66,9 +67,8 @@ static struct {
                /* PID of external command called e.g. by text widget to edit text.
                   ON exit, cmd_caller->vtable->cmd_return is called with the text
                   the external command wrote to a file. */
       -        /*IMPORTANT: this needs to be checked whenever a widget is destroyed!
       -        FIXME: allow option to instead return output of command */
       -        ltk_array(cmd) *cmds;
       +        /*FIXME: this needs to be checked whenever a widget is destroyed!*/
       +        ltk_array(cmdinfo) *cmds;
                size_t cur_kbd;
        } shared_data = {NULL, NULL, NULL, NULL, NULL, NULL, 0};
        
       @@ -97,59 +97,12 @@ typedef struct {
           knows if I'll need them again sometime... */
        static ltk_widget_funcs widget_funcs[] = {
                {
       -                .name = "box",
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "button",
       -                .cleanup = NULL,
       -        },
       -        {
                        .name = "entry",
                        .cleanup = &ltk_entry_cleanup,
                },
                {
       -                .name = "grid",
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "label",
       -                .cleanup = NULL,
       -        },
       -        {
       -                /* FIXME: this is actually image_widget */
       -                .name = "image",
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "menu",
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "menuentry",
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "submenu",
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "submenuentry",
       -                .cleanup = NULL,
       -                 /*
       -                 This "widget" is only needed to have separate styles for regular
       -                   menu entries and submenu entries. "submenu" is just an alias for
       -                   "menu" in most cases - it's just needed when creating a menu to
       -                   decide if it's a submenu or not.
       -                   FIXME: is that even necessary? Why can't it just decide if it's
       -                   a submenu based on whether it has a parent or not?
       -                   -> I guess right-click menus are also just submenus, so they
       -                   need to set it explicitly, but wasn't there another reason? 
       -                 */
       -        },
       -        {
       -                .name = "scrollbar",
       -                .cleanup = NULL,
       +                .name = "combobox",
       +                .cleanup = &ltk_combobox_cleanup,
                },
                {
                        /* Handler for window theme. */
       @@ -202,7 +155,7 @@ ltk_init(void) {
                ltk_image_init(shared_data.renderdata, 1024 * 1024 * 4);
                shared_data.windows = ltk_array_create(window, 1);
                shared_data.rwindows = ltk_array_create(rwindow, 1);
       -        shared_data.cmds = ltk_array_create(cmd, 1);
       +        shared_data.cmds = ltk_array_create(cmdinfo, 1);
                return 0; /* FIXME: or maybe 1? */
        }
        
       @@ -237,28 +190,37 @@ ltk_mainloop_step(int limit_framerate) {
                int pid = -1;
                int wstatus = 0;
                /* FIXME: kill all children on exit? */
       +        /* -> at least unlink any files? */
                if ((pid = waitpid(-1, &wstatus, WNOHANG)) > 0) {
                        ltk_cmdinfo *info;
                        /* FIXME: should commands be split into read/write and block write commands during external editing? */
                        for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
                                info = &(ltk_array_get(shared_data.cmds, i));
                                if (info->pid == pid) {
       +                                /* FIXME: actually NULL this when widgets are destroyed */
                                        if (!info->caller) {
                                                ltk_warn("Widget disappeared while text was being edited in external program\n");
                                        /* FIXME: call overwritten cmd_return! */
                                        } else if (info->caller->vtable->cmd_return) {
                                                size_t file_len = 0;
                                                char *errstr = NULL;
       -                                        char *contents = ltk_read_file(info->tmpfile, &file_len, &errstr);
       +                                        char *filename = info->outfile ? info->outfile : info->infile;
       +                                        char *contents = ltk_read_file(filename, &file_len, &errstr);
                                                if (!contents) {
       -                                                ltk_warn("Unable to read file '%s' written by external command: %s\n", info->tmpfile, errstr);
       +                                                ltk_warn("Unable to read file '%s' written by external command: %s\n", filename, errstr);
                                                } else {
                                                        info->caller->vtable->cmd_return(info->caller, contents, file_len);
                                                        ltk_free0(contents);
                                                }
                                        }
       -                                ltk_free0(info->tmpfile);
       -                                ltk_array_delete(cmd, shared_data.cmds, i, 1);
       +                                /* FIXME: error checking */
       +                                unlink(info->infile);
       +                                ltk_free(info->infile);
       +                                if (info->outfile) {
       +                                        unlink(info->outfile);
       +                                        ltk_free(info->outfile);
       +                                }
       +                                ltk_array_delete(cmdinfo, shared_data.cmds, i, 1);
                                        break;
                                }
                        }
       @@ -341,9 +303,11 @@ ltk_deinit(void) {
                if (shared_data.cmds) {
                        for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
                                /* FIXME: maybe kill child processes? */
       -                        ltk_free((ltk_array_get(shared_data.cmds, i)).tmpfile);
       +                        ltk_free((ltk_array_get(shared_data.cmds, i)).infile);
       +                        if (ltk_array_get(shared_data.cmds, i).outfile)
       +                                ltk_free((ltk_array_get(shared_data.cmds, i)).outfile);
                        }
       -                ltk_array_destroy(cmd, shared_data.cmds);
       +                ltk_array_destroy(cmdinfo, shared_data.cmds);
                }
                shared_data.cmds = NULL;
                if (shared_data.windows) {
       @@ -462,38 +426,140 @@ ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg da
                return id;
        }
        
       +LTK_ARRAY_INIT_DECL_STATIC(str, char *)
       +LTK_ARRAY_INIT_IMPL_STATIC(str, char *)
       +
       +static void
       +str_free_helper(char *elem) {
       +        ltk_free(elem);
       +}
       +
        int
       -ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen) {
       +ltk_call_cmd(ltk_widget *caller, ltk_array(cmd) *cmd, const char *text, size_t textlen) {
       +        /* FIXME: maybe support stdin/stdout without temporary files by just piping directly */
                /* FIXME: support environment variable $TMPDIR */
       -        ltk_cmdinfo info = {NULL, NULL, -1};
       -        info.tmpfile = ltk_strdup("/tmp/ltk.XXXXXX");
       -        int fd = mkstemp(info.tmpfile);
       -        if (fd == -1) {
       -                ltk_warn_errno("Unable to create temporary file while trying to run command '%.*s'\n", (int)cmdlen, cmd);
       -                ltk_free0(info.tmpfile);
       -                return 1;
       +        ltk_cmdinfo info = {
       +                .caller = NULL, .infile = NULL, .outfile = NULL, .pid = -1
       +        };
       +        ltk_array(str) *cmdstr = ltk_array_create(str, 4);
       +        txtbuf *tmpbuf = txtbuf_new();
       +        int needs_stdin = 1;
       +        int needs_stdout = 1;
       +
       +        int infd = -1, outfd = -1;
       +
       +        info.infile = ltk_strdup("/tmp/ltk.XXXXXX");
       +        infd = mkstemp(info.infile);
       +        if (infd == -1) {
       +                ltk_warn_errno("Unable to create temporary input file while trying to run command.");
       +                ltk_free(info.infile);
       +                info.infile = NULL; /* so it isn't unlinked below */
       +                goto error;
                }
       -        close(fd);
                /* FIXME: give file descriptor directly to modified version of ltk_write_file */
                char *errstr = NULL;
       -        if (ltk_write_file(info.tmpfile, text, textlen, &errstr)) {
       -                ltk_warn("Unable to write to file '%s' while trying to run command '%.*s': %s\n", info.tmpfile, (int)cmdlen, cmd, errstr);
       -                unlink(info.tmpfile);
       -                ltk_free0(info.tmpfile);
       -                return 1;
       +        if (ltk_write_file(info.infile, text, textlen, &errstr)) {
       +                ltk_warn("Unable to write to temporary input file '%s' while trying to run command.", info.infile, errstr);
       +                goto error;
                }
       -        int pid = -1;
       -        if ((pid = ltk_parse_run_cmd(cmd, cmdlen, info.tmpfile)) <= 0) {
       -                /* FIXME: errno */
       -                ltk_warn("Unable to run command '%.*s'\n", (int)cmdlen, cmd);
       -                unlink(info.tmpfile);
       -                ltk_free0(info.tmpfile);
       -                return 1;
       +
       +        for (size_t i = 0; i < ltk_array_len(cmd); i++) {
       +                ltk_array(cmdpiece) *pa = ltk_array_get(cmd, i);
       +                for (size_t j = 0; j < ltk_array_len(pa); j++) {
       +                        struct ltk_cmd_piece p = ltk_array_get(pa, j);
       +                        switch (p.type) {
       +                        case LTK_CMD_TEXT:
       +                                txtbuf_append(tmpbuf, p.text);
       +                                break;
       +                        case LTK_CMD_INOUT_FILE:
       +                                needs_stdout = 0;
       +                                /* fall through */
       +                        case LTK_CMD_INPUT_FILE:
       +                                needs_stdin = 0;
       +                                txtbuf_append(tmpbuf, info.infile);
       +                                break;
       +                        case LTK_CMD_OUTPUT_FILE:
       +                                needs_stdout = 0;
       +                                if (!info.outfile) {
       +                                        info.outfile = ltk_strdup("/tmp/ltk.XXXXXX");
       +                                        outfd = mkstemp(info.outfile);
       +                                        if (outfd == -1) {
       +                                                ltk_warn_errno("Unable to create temporary output file while trying to run command.");
       +                                                ltk_free(info.outfile);
       +                                                info.outfile = NULL; /* so it isn't unlinked below */
       +                                                goto error;
       +                                        }
       +                                }
       +                                txtbuf_append(tmpbuf, info.outfile);
       +                                break;
       +                        default:
       +                                ltk_warn("Invalid command piece type. This should not happen.");
       +                                goto error;
       +                        }
       +                }
       +                ltk_array_append(str, cmdstr, txtbuf_get_textcopy(tmpbuf));
       +                txtbuf_clear(tmpbuf);
       +        }
       +        /* if no output file was specified, we still need to create it for stdout */
       +        if (needs_stdout) {
       +                info.outfile = ltk_strdup("/tmp/ltk.XXXXXX");
       +                outfd = mkstemp(info.outfile);
       +                if (outfd == -1) {
       +                        ltk_warn_errno("Unable to create temporary output file while trying to run command.");
       +                        ltk_free(info.outfile);
       +                        info.outfile = NULL; /* so it isn't unlinked below */
       +                        goto error;
       +                }
       +        }
       +        ltk_array_append(str, cmdstr, NULL); /* necessary for execve */
       +        txtbuf_destroy(tmpbuf);
       +        tmpbuf = NULL;
       +
       +        int fret = -1;
       +        if ((fret = fork()) < 0) {
       +                ltk_warn("Unable to fork\n");
       +                goto error;
       +        } else if (fret == 0) {
       +                if (needs_stdin) {
       +                        if (dup2(infd, fileno(stdin)) == -1)
       +                                ltk_fatal("Unable to set up stdin in child process.");
       +                }
       +                if (needs_stdout) {
       +                        int fd = outfd == -1 ? infd : outfd;
       +                        if (dup2(fd, fileno(stdout)) == -1)
       +                                ltk_fatal("Unable to set up stdout in child process.");
       +                }
       +                if (execvp(cmdstr->buf[0], cmdstr->buf) == -1)
       +                        ltk_fatal("Unable to exec external command.");
                }
       -        info.pid = pid;
       +        ltk_array_destroy_deep(str, cmdstr, &str_free_helper);
       +
       +        info.pid = fret;
                info.caller = caller;
       -        ltk_array_append(cmd, shared_data.cmds, info);
       +        ltk_array_append(cmdinfo, shared_data.cmds, info);
       +
       +        if (infd != -1)
       +                close(infd); /* FIXME: error checking also on close */
       +        if (outfd != -1)
       +                close(outfd);
                return 0;
       +error:
       +        if (infd != -1)
       +                close(infd); /* FIXME: error checking also on close and unlink */
       +        if (outfd != -1)
       +                close(outfd);
       +        if (tmpbuf)
       +                txtbuf_destroy(tmpbuf);
       +        if (info.infile) {
       +                unlink(info.infile);
       +                ltk_free(info.infile);
       +        }
       +        if (info.outfile) {
       +                unlink(info.outfile);
       +                ltk_free(info.outfile);
       +        }
       +        ltk_array_destroy_deep(str, cmdstr, &str_free_helper);
       +        return 1;
        }
        
        static void
   DIR diff --git a/src/ltk/ltk.h b/src/ltk/ltk.h
       @@ -43,12 +43,6 @@ int ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_ar
        ltk_window *ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h);
        void ltk_window_destroy(ltk_widget *self, int shallow);
        
       -/* FIXME: allow piping text instead of writing to temporary file */
       -/* FIXME: how to avoid bad things happening while external program open? maybe store cmd widget somewhere (but could be multiple!) and check if widget to destroy is one of those 
       --> alternative: store all widgets in array and only give out IDs, then when returning from cmd, widget is already destroyed and can be ignored
       --> first option maybe just set callback, etc. of current cmd to NULL so widget can still be destroyed */
       -int ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen);
       -
        /* convenience function to use the default text context */
        ltk_text_line *ltk_text_line_create_default(const char *font, int font_size, char *text, int take_over_text, int width);
        ltk_text_line *ltk_text_line_create_const_text_default(const char *font, int font_size, const char *text, int width);
   DIR diff --git a/src/ltk/memory.h b/src/ltk/memory.h
       @@ -17,8 +17,6 @@
        #ifndef LTK_MEMORY_H
        #define LTK_MEMORY_H
        
       -/* FIXME: Move ltk_warn, etc. to util.* */
       -
        #include <stdlib.h>
        
        #if MEMDEBUG == 1
   DIR diff --git a/src/ltk/menu.c b/src/ltk/menu.c
       @@ -104,7 +104,6 @@ static int ltk_menu_motion_notify(ltk_widget *self, ltk_motion_event *event);
        static int ltk_menu_mouse_enter(ltk_widget *self, ltk_motion_event *event);
        static int ltk_menu_mouse_leave(ltk_widget *self, ltk_motion_event *event);
        static void shrink_entries(ltk_menu *menu);
       -static size_t get_entry(ltk_menu *menu, ltk_menuentry *entry);
        static void ltk_menu_destroy(ltk_widget *self, int shallow);
        
        static ltk_menu *ltk_menu_create_base(ltk_window *window, int is_submenu);
       @@ -321,6 +320,11 @@ ltk_menuentry_get_child(ltk_widget *self) {
                return NULL;
        }
        
       +const char *
       +ltk_menuentry_get_text(ltk_menuentry *entry) {
       +        return ltk_text_line_get_text(entry->text_line);
       +}
       +
        static void
        ltk_menuentry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
                /* FIXME: figure out how hidden should work */
       @@ -731,20 +735,19 @@ popup_active_menu(ltk_menuentry *e) {
                ltk_rect menu_rect = e->widget.lrect;
                ltk_point entry_global = ltk_widget_pos_to_global(LTK_CAST_WIDGET(e), 0, 0);
                ltk_point menu_global;
       -        if (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
       -                ltk_menu *menu = LTK_CAST_MENU(e->widget.parent);
       +        if (LTK_CAST_WIDGET(e)->parent && LTK_CAST_WIDGET(e)->parent->vtable->type == LTK_WIDGET_MENU) {
       +                ltk_menu *menu = LTK_CAST_MENU(LTK_CAST_WIDGET(e)->parent);
                        in_submenu = menu->is_submenu;
                        was_opened_left = menu->was_opened_left;
                        menu_rect = menu->widget.lrect;
       -                menu_global = ltk_widget_pos_to_global(e->widget.parent, 0, 0);
       +                menu_global = ltk_widget_pos_to_global(LTK_CAST_WIDGET(e)->parent, 0, 0);
                } else {
       -                menu_global = ltk_widget_pos_to_global(&e->widget, 0, 0);
       +                menu_global = ltk_widget_pos_to_global(LTK_CAST_WIDGET(e), 0, 0);
                }
                int win_w = e->widget.window->rect.w;
                int win_h = e->widget.window->rect.h;
                ltk_menu *submenu = e->submenu;
                ltk_widget_recalc_ideal_size(LTK_CAST_WIDGET(submenu));
       -        ltk_widget_resize(LTK_CAST_WIDGET(submenu));
                int ideal_w = submenu->widget.ideal_w;
                int ideal_h = submenu->widget.ideal_h;
                int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h;
       @@ -842,14 +845,14 @@ popup_active_menu(ltk_menuentry *e) {
                submenu->widget.lrect.y = y_final;
                submenu->widget.lrect.w = w_final;
                submenu->widget.lrect.h = h_final;
       -        submenu->widget.crect = submenu->widget.lrect;
       +        submenu->widget.crect = LTK_CAST_WIDGET(submenu)->lrect;
                submenu->widget.dirty = 1;
                submenu->widget.hidden = 0;
                submenu->popup_submenus = 0;
                submenu->unpopup_submenus_on_hide = 1;
       -        ltk_menu_resize(&submenu->widget);
       -        ltk_window_register_popup(e->widget.window, (ltk_widget *)submenu);
       -        ltk_window_invalidate_widget_rect(submenu->widget.window, &submenu->widget);
       +        ltk_widget_resize(LTK_CAST_WIDGET(submenu));
       +        ltk_window_register_popup(LTK_CAST_WIDGET(e)->window, LTK_CAST_WIDGET(submenu));
       +        ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(submenu)->window, LTK_CAST_WIDGET(submenu));
        }
        
        static void
       @@ -1126,15 +1129,16 @@ shrink_entries(ltk_menu *menu) {
                }
        }
        
       -int
       +ltk_menuentry *
        ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx) {
                if (idx >= menu->num_entries)
       -                return 1; /* invalid index */
       +                return NULL; /* invalid index */
                menu->entries[idx]->widget.parent = NULL;
                /* I don't think this is needed because the entry isn't shown
                   anywhere. Its size will be recalculated once it is added
                   to a menu again. */
                /* ltk_menuentry_recalc_ideal_size_with_notification(menu->entries[idx]); */
       +        ltk_menuentry *ret = menu->entries[idx];
                memmove(
                    menu->entries + idx,
                    menu->entries + idx + 1,
       @@ -1143,11 +1147,11 @@ ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx) {
                menu->num_entries--;
                shrink_entries(menu);
                recalc_ideal_menu_size_with_notification(LTK_CAST_WIDGET(menu), NULL);
       -        return 0;
       +        return ret;
        }
        
       -static size_t
       -get_entry(ltk_menu *menu, ltk_menuentry *entry) {
       +size_t
       +ltk_menu_get_entry_index(ltk_menu *menu, ltk_menuentry *entry) {
                for (size_t i = 0; i < menu->num_entries; i++) {
                        if (menu->entries[i] == entry)
                                return i;
       @@ -1155,12 +1159,27 @@ get_entry(ltk_menu *menu, ltk_menuentry *entry) {
                return SIZE_MAX;
        }
        
       +size_t
       +ltk_menu_get_num_entries(ltk_menu *menu) {
       +        return menu->num_entries;
       +}
       +
       +ltk_menuentry *
       +ltk_menu_get_entry(ltk_menu *menu, size_t idx) {
       +        if (idx >= menu->num_entries)
       +                return NULL;
       +        return menu->entries[idx];
       +}
       +
        int
        ltk_menu_remove_entry(ltk_menu *menu, ltk_menuentry *entry) {
       -        size_t idx = get_entry(menu, entry);
       +        size_t idx = ltk_menu_get_entry_index(menu, entry);
                if (idx >= menu->num_entries)
                        return 1;
       -        return ltk_menu_remove_entry_index(menu, idx);
       +        ltk_menuentry *ret = ltk_menu_remove_entry_index(menu, idx);
       +        if (!ret) /* shouldn't be possible */
       +                return 1;
       +        return 0;
        }
        
        static int
       @@ -1212,7 +1231,7 @@ ltk_menu_nearest_child(ltk_widget *self, ltk_rect rect) {
           is already at bottom of respective menu - the top-level menu will give the first submenu in
           the current active hierarchy as child widget again, and nearest_child on that submenu will
           (probably) give the bottom widget again, so nothing changes except that all submenus except
       -   for the first and second one disappeare */
       +   for the first and second one disappear */
        static ltk_widget *
        ltk_menu_nearest_child_left(ltk_widget *self, ltk_widget *widget) {
                ltk_menu *menu = LTK_CAST_MENU(self);
   DIR diff --git a/src/ltk/menu.h b/src/ltk/menu.h
       @@ -73,10 +73,14 @@ ltk_menu *ltk_submenu_create(ltk_window *window);
        ltk_menuentry *ltk_menuentry_create(ltk_window *window, const char *text);
        int ltk_menuentry_attach_submenu(ltk_menuentry *e, ltk_menu *submenu);
        int ltk_menuentry_detach_submenu(ltk_menuentry *e);
       +const char *ltk_menuentry_get_text(ltk_menuentry *entry);
        int ltk_menu_insert_entry(ltk_menu *menu, ltk_menuentry *entry, size_t idx);
        int ltk_menu_add_entry(ltk_menu *menu, ltk_menuentry *entry);
       -int ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx);
       +ltk_menuentry *ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx);
        int ltk_menu_remove_entry(ltk_menu *menu, ltk_menuentry *entry);
        void ltk_menu_remove_all_entries(ltk_menu *menu);
       +size_t ltk_menu_get_num_entries(ltk_menu *menu);
       +ltk_menuentry *ltk_menu_get_entry(ltk_menu *menu, size_t idx);
       +size_t ltk_menu_get_entry_index(ltk_menu *menu, ltk_menuentry *entry);
        
        #endif /* LTK_MENU_H */
   DIR diff --git a/src/ltk/text.h b/src/ltk/text.h
       @@ -40,6 +40,8 @@ void ltk_text_line_get_size(ltk_text_line *tl, int *w, int *h);
        void ltk_text_line_destroy(ltk_text_line *tl);
        /* FIXME: length of text */
        void ltk_text_line_set_text(ltk_text_line *line, char *text, int take_over_text);
       +void ltk_text_line_set_const_text(ltk_text_line *line, const char *text);
       +const char *ltk_text_line_get_text(ltk_text_line *line);
        
        /* Draw the entire line to a surface. */
        /* FIXME: Some widgets rely on this to not fail when negative coordinates are given or
   DIR diff --git a/src/ltk/text_pango.c b/src/ltk/text_pango.c
       @@ -102,6 +102,16 @@ ltk_text_line_set_text(ltk_text_line *tl, char *text, int take_over_text) {
        }
        
        void
       +ltk_text_line_set_const_text(ltk_text_line *tl, const char *text) {
       +        ltk_text_line_set_text(tl, ltk_strdup(text), 1);
       +}
       +
       +const char *
       +ltk_text_line_get_text(ltk_text_line *tl) {
       +        return tl->text;
       +}
       +
       +void
        ltk_text_line_set_font_size(ltk_text_line *tl, int font_size) {
                if (font_size == tl->font_size)
                        return;
   DIR diff --git a/src/ltk/txtbuf.c b/src/ltk/txtbuf.c
       @@ -135,6 +135,16 @@ txtbuf_get_textcopy(txtbuf *buf) {
                return buf->text ? ltk_strndup(buf->text, buf->len) : ltk_strdup("");
        }
        
       +const char *
       +txtbuf_get_text(txtbuf *buf) {
       +        return buf->text;
       +}
       +
       +size_t
       +txtbuf_len(txtbuf *buf) {
       +        return buf->len;
       +}
       +
        /* FIXME: proper "normalize" function to add nul-termination if needed */
        int
        txtbuf_cmp(txtbuf *buf1, txtbuf *buf2) {
   DIR diff --git a/src/ltk/txtbuf.h b/src/ltk/txtbuf.h
       @@ -113,6 +113,19 @@ txtbuf *txtbuf_dup(txtbuf *src);
        char *txtbuf_get_textcopy(txtbuf *buf);
        
        /*
       + * Get text stored in 'buf'.
       + * The returned text belongs to the txtbuf and must not be changed.
       + * The returned text may be invalidated as soon as any other
       + * functions are called on the txtbuf.
       + */
       +const char *txtbuf_get_text(txtbuf *buf);
       +
       +/*
       + * Get the length of the text stored in 'buf'.
       + */
       +size_t txtbuf_len(txtbuf *buf);
       +
       +/*
         * Clear the text, but do not reduce the internal capacity
         * (for efficiency if it will be filled up again anyways).
         */
   DIR diff --git a/src/ltk/util.c b/src/ltk/util.c
       @@ -85,194 +85,6 @@ errorclose:
                return 1;
        }
        
       -/* FIXME: maybe have a few standard array types defined somewhere else */
       -LTK_ARRAY_INIT_DECL_STATIC(cmd, char *)
       -LTK_ARRAY_INIT_IMPL_STATIC(cmd, char *)
       -
       -static void
       -free_helper(char *ptr) {
       -        ltk_free(ptr);
       -}
       -
       -/* FIXME: this is really ugly */
       -/* FIXME: parse command only once in beginning instead of each time it is run? */
       -/* FIXME: this handles double-quote, but the config parser already uses that, so
       -   it's kind of weird because it's parsed twice (also backslashes are parsed twice). */
       -int
       -ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename) {
       -        int bs = 0;
       -        int in_sqstr = 0;
       -        int in_dqstr = 0;
       -        int in_ws = 1;
       -        char c;
       -        size_t cur_start = 0;
       -        int offset = 0;
       -        txtbuf *cur_arg = txtbuf_new();
       -        ltk_array(cmd) *cmd = ltk_array_create(cmd, 4);
       -        char *cmdcopy = ltk_strndup(cmdtext, len);
       -        for (size_t i = 0; i < len; i++) {
       -                c = cmdcopy[i];
       -                if (c == '\\') {
       -                        if (bs) {
       -                                offset++;
       -                                bs = 0;
       -                        } else {
       -                                bs = 1;
       -                        }
       -                } else if (isspace(c)) {
       -                        if (!in_sqstr && !in_dqstr) {
       -                                if (bs) {
       -                                        if (in_ws) {
       -                                                in_ws = 0;
       -                                                cur_start = i;
       -                                                offset = 0;
       -                                        } else {
       -                                                offset++;
       -                                        }
       -                                        bs = 0;
       -                                } else if (!in_ws) {
       -                                        /* FIXME: shouldn't this be < instead of <=? */
       -                                        if (cur_start <= i - offset)
       -                                                txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset);
       -                                        /* FIXME: cmd is named horribly */
       -                                        ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg));
       -                                        txtbuf_clear(cur_arg);
       -                                        in_ws = 1;
       -                                        offset = 0;
       -                                }
       -                        /* FIXME: parsing weird here - bs just ignored */
       -                        } else if (bs) {
       -                                bs = 0;
       -                        }
       -                } else if (c == '%') {
       -                        if (bs) {
       -                                if (in_ws) {
       -                                        cur_start = i;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                }
       -                                bs = 0;
       -                        } else if (!in_sqstr && filename && i < len - 1 && cmdcopy[i + 1] == 'f') {
       -                                if (!in_ws && cur_start < i - offset)
       -                                        txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset);
       -                                txtbuf_append(cur_arg, filename);
       -                                i++;
       -                                cur_start = i + 1;
       -                                offset = 0;
       -                        } else if (in_ws) {
       -                                cur_start = i;
       -                                offset = 0;
       -                        }
       -                        in_ws = 0;
       -                } else if (c == '"') {
       -                        if (in_sqstr) {
       -                                bs = 0;
       -                        } else if (bs) {
       -                                if (in_ws) {
       -                                        cur_start = i;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                }
       -                                bs = 0;
       -                        } else if (in_dqstr) {
       -                                offset++;
       -                                in_dqstr = 0;
       -                                continue;
       -                        } else {
       -                                in_dqstr = 1;
       -                                if (in_ws) {
       -                                        cur_start = i + 1;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                        continue;
       -                                }
       -                        }
       -                        in_ws = 0;
       -                } else if (c == '\'') {
       -                        if (in_dqstr) {
       -                                bs = 0;
       -                        } else if (bs) {
       -                                if (in_ws) {
       -                                        cur_start = i;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                }
       -                                bs = 0;
       -                        } else if (in_sqstr) {
       -                                offset++;
       -                                in_sqstr = 0;
       -                                continue;
       -                        } else {
       -                                in_sqstr = 1;
       -                                if (in_ws) {
       -                                        cur_start = i + 1;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                        continue;
       -                                }
       -                        }
       -                        in_ws = 0;
       -                } else if (bs) {
       -                        if (!in_sqstr && !in_dqstr) {
       -                                if (in_ws) {
       -                                        cur_start = i;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                }
       -                        }
       -                        bs = 0;
       -                        in_ws = 0;
       -                } else {
       -                        if (in_ws) {
       -                                cur_start = i;
       -                                offset = 0;
       -                        }
       -                        in_ws = 0;
       -                }
       -                cmdcopy[i - offset] = cmdcopy[i];
       -        }
       -        if (in_sqstr || in_dqstr) {
       -                ltk_warn("Unterminated string in command\n");
       -                goto error;
       -        }
       -        if (!in_ws) {
       -                if (cur_start <= len - offset)
       -                        txtbuf_appendn(cur_arg, cmdcopy + cur_start, len - cur_start - offset);
       -                ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg));
       -        }
       -        if (cmd->len == 0) {
       -                ltk_warn("Empty command\n");
       -                goto error;
       -        }
       -        ltk_array_append(cmd, cmd, NULL); /* necessary for execvp */
       -        int fret = -1;
       -        if ((fret = fork()) < 0) {
       -                ltk_warn("Unable to fork\n");
       -                goto error;
       -        } else if (fret == 0) {
       -                if (execvp(cmd->buf[0], cmd->buf) == -1) {
       -                        /* FIXME: what to do on error here? */
       -                        exit(1);
       -                }
       -        } else {
       -                ltk_free(cmdcopy);
       -                txtbuf_destroy(cur_arg);
       -                ltk_array_destroy_deep(cmd, cmd, &free_helper);
       -                return fret;
       -        }
       -error:
       -        ltk_free(cmdcopy);
       -        txtbuf_destroy(cur_arg);
       -        ltk_array_destroy_deep(cmd, cmd, &free_helper);
       -        return -1;
       -}
       -
        /* If `needed` is larger than `*alloc_size`, resize `*str` to
           `max(needed, *alloc_size * 2)`. Aborts program on error. */
        void
   DIR diff --git a/src/ltk/widget.h b/src/ltk/widget.h
       @@ -47,6 +47,7 @@ typedef enum {
                LTK_WIDGET_SCROLLBAR,
                LTK_WIDGET_CHECKBUTTON,
                LTK_WIDGET_RADIOBUTTON,
       +        LTK_WIDGET_COMBOBOX,
                LTK_NUM_WIDGETS,
        } ltk_widget_type;
        
       @@ -188,6 +189,7 @@ typedef struct {
        #define LTK_CAST_BOX(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BOX), (ltk_box *)(w))
        #define LTK_CAST_CHECKBUTTON(w) (ltk_assert(w->vtable->type == LTK_WIDGET_CHECKBUTTON), (ltk_checkbutton *)(w))
        #define LTK_CAST_RADIOBUTTON(w) (ltk_assert(w->vtable->type == LTK_WIDGET_RADIOBUTTON), (ltk_radiobutton *)(w))
       +#define LTK_CAST_COMBOBOX(w) (ltk_assert(w->vtable->type == LTK_WIDGET_COMBOBOX), (ltk_combobox *)(w))
        
        /* FIXME: a bit weird because window never gets some of these signals */
        #define LTK_WIDGET_SIGNAL_KEY_PRESS          1
   DIR diff --git a/src/ltk/widget_internal.h b/src/ltk/widget_internal.h
       @@ -35,6 +35,14 @@ void ltk_submenu_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
        void ltk_submenuentry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
        void ltk_scrollbar_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
        
       +void ltk_combobox_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
       +void ltk_combobox_cleanup(void);
       +void ltk_combobox_get_keybinding_parseinfo(
       +        ltk_keybinding_cb **press_cbs_ret, size_t *press_len_ret,
       +        ltk_keybinding_cb **release_cbs_ret, size_t *release_len_ret,
       +        ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret
       +);
       +
        void ltk_entry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len);
        void ltk_entry_cleanup(void);
        void ltk_entry_get_keybinding_parseinfo(
       @@ -53,4 +61,9 @@ void ltk_window_get_keybinding_parseinfo(
                ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret
        );
        
       +/* FIXME: how to avoid bad things happening while external program open? maybe store cmd widget somewhere (but could be multiple!) and check if widget to destroy is one of those
       +-> alternative: store all widgets in array and only give out IDs, then when returning from cmd, widget is already destroyed and can be ignored
       +-> first option maybe just set callback, etc. of current cmd to NULL so widget can still be destroyed */
       +int ltk_call_cmd(ltk_widget *caller, ltk_array(cmd) *cmd, const char *text, size_t textlen);
       +
        #endif /* LTK_WIDGET_INTERNAL_H */
   DIR diff --git a/src/ltk/window.c b/src/ltk/window.c
       @@ -193,19 +193,16 @@ ltk_window_key_press_event(ltk_widget *self, ltk_key_event *event) {
                if (!keypresses)
                        return 1;
                ltk_keypress_binding *b = NULL;
       +        /* FIXME: move into separate function and share between window, entry, etc. */
                for (size_t i = 0; i < ltk_array_len(keypresses); i++) {
                        b = &ltk_array_get(keypresses, i).b;
       -                if (b->mods != event->modmask || (!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) {
       +                if ((!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled))
                                continue;
       -                } else if (b->text) {
       -                        if (event->mapped && !strcmp(b->text, event->mapped))
       -                                handled |= ltk_array_get(keypresses, i).cb.func(LTK_CAST_WIDGET(window), event);
       -                } else if (b->rawtext) {
       -                        if (event->text && !strcmp(b->text, event->text))
       -                                handled |= ltk_array_get(keypresses, i).cb.func(LTK_CAST_WIDGET(window), event);
       -                } else if (b->sym != LTK_KEY_NONE) {
       -                        if (event->sym == b->sym)
       -                                handled |= ltk_array_get(keypresses, i).cb.func(LTK_CAST_WIDGET(window), event);
       +                if ((b->mods == event->modmask && b->sym != LTK_KEY_NONE && b->sym == event->sym) ||
       +                    (b->mods == (event->modmask & ~LTK_MOD_SHIFT) &&
       +                     ((b->text && event->mapped && !strcmp(b->text, event->mapped)) ||
       +                      (b->rawtext && event->text && !strcmp(b->rawtext, event->text))))) {
       +                        handled |= ltk_array_get(keypresses, i).cb.func(LTK_CAST_WIDGET(window), event);
                        }
                }
                return 1;