URI: 
       tAdd basic support for external line editor - ltk - Socket-based GUI for X11 (WIP)
  HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit e714a12f36b44d6b67fcad122c858af6b903bdb2
   DIR parent 59e4368e073cc9c2a99bfb26f928e120502c5ce5
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Sun, 27 Aug 2023 21:55:58 +0200
       
       Add basic support for external line editor
       
       Diffstat:
         M .ltk/ltk.cfg                        |       3 ++-
         M Makefile                            |       2 +-
         M src/config.c                        |       9 +++++++--
         M src/config.h                        |       1 +
         M src/entry.c                         |      43 ++++++++++++++++++++++++++-----
         M src/ltk.h                           |       9 +++++++++
         M src/ltkd.c                          |      70 ++++++++++++++++++++++++++++++-
         M src/text_pango.c                    |       4 +++-
         M src/text_stb.c                      |       5 +++--
         M src/theme.c                         |       5 ++++-
         M src/txtbuf.c                        |      19 ++++++++++++-------
         M src/txtbuf.h                        |      20 +++++++++++++-------
         M src/util.c                          |     252 +++++++++++++++++++++++++++++--
         M src/util.h                          |       6 ++++--
         M src/widget.h                        |       1 +
       
       15 files changed, 404 insertions(+), 45 deletions(-)
       ---
   DIR diff --git a/.ltk/ltk.cfg b/.ltk/ltk.cfg
       t@@ -1,9 +1,9 @@
        [general]
        explicit-focus = true
        all-activatable = true
       +line-editor = "st -e vi %f"
        # In future:
        # text-editor = ...
       -# line-editor = ...
        
        [key-binding:widget]
        # In future:
       t@@ -44,6 +44,7 @@ bind-keypress expand-selection-right sym right mods shift
        bind-keypress selection-to-clipboard text c mods ctrl
        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
        
        # default mapping (just to silence warnings)
        [key-mapping]
   DIR diff --git a/Makefile b/Makefile
       t@@ -109,7 +109,7 @@ src/ltkd: $(OBJ)
                $(CC) -o $@ $(OBJ) $(LTK_LDFLAGS)
        
        src/ltkc: src/ltkc.o src/util.o src/memory.o
       -        $(CC) -o $@ src/ltkc.o src/util.o src/memory.o $(LTK_LDFLAGS)
       +        $(CC) -o $@ src/ltkc.o src/util.o src/memory.o src/txtbuf.o $(LTK_LDFLAGS)
        
        $(OBJ) : $(HDR)
        
   DIR diff --git a/src/config.c b/src/config.c
       t@@ -435,6 +435,7 @@ destroy_config(ltk_config *c) {
                        }
                        ltk_free(c->mappings[i].mappings);
                }
       +        ltk_free(c->general.line_editor);
                ltk_free(c->mappings);
                ltk_free(c);
        }
       t@@ -494,6 +495,7 @@ load_from_text(
                config->mappings_alloc = config->mappings_len = 0;
                config->general.explicit_focus = 0;
                config->general.all_activatable = 0;
       +        config->general.line_editor = NULL;
        
                struct lexstate s = {filename, file_contents, len, 0, 1, 0};
                struct token tok = next_token(&s);
       t@@ -548,6 +550,8 @@ load_from_text(
                                                                msg = "Invalid boolean setting";
                                                                goto error;
                                                        }
       +                                        } else if (str_array_equal("line-editor", prev2tok.text, prev2tok.len)) {
       +                                                config->general.line_editor = ltk_strndup(tok.text, tok.len);
                                                } else {
                                                        msg = "Invalid setting";
                                                        goto error;
       t@@ -658,9 +662,10 @@ ltk_config_parsefile(
            keyrelease_binding_handler release_handler,
            char **errstr) {
                unsigned long len = 0;
       -        char *file_contents = ltk_read_file(filename, &len);
       +        char *ferrstr = NULL;
       +        char *file_contents = ltk_read_file(filename, &len, &ferrstr);
                if (!file_contents) {
       -                *errstr = ltk_print_fmt("Unable to open file \"%s\"", filename);
       +                *errstr = ltk_print_fmt("Unable to open file \"%s\": %s", filename, ferrstr);
                        return 1;
                }
                int ret = load_from_text(filename, file_contents, len, press_handler, release_handler, errstr);
   DIR diff --git a/src/config.h b/src/config.h
       t@@ -36,6 +36,7 @@ typedef struct {
        } ltk_language_mapping;
        
        typedef struct {
       +        char *line_editor;
                char explicit_focus;
                char all_activatable;
        } ltk_general_config;
   DIR diff --git a/src/entry.c b/src/entry.c
       t@@ -14,7 +14,7 @@
         * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
         */
        
       -/* FIXME: allow opening text in external program */
       +/* FIXME: mouse actions for expanding selection (shift+click) */
        /* FIXME: cursors jump weirdly with bidi text
           (need to support strong/weak cursors in pango backend) */
        /* FIXME: set imspot - needs to be standardized so widgets don't all do their own thing */
       t@@ -58,6 +58,7 @@ static int ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event);
        static int ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event);
        static int ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event);
        static int ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event);
       +static void ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len);
        
        /* FIXME: also allow binding key release, not just press */
        typedef void (*cb_func)(ltk_entry *, ltk_key_event *);
       t@@ -78,9 +79,10 @@ static void paste_clipboard(ltk_entry *entry, ltk_key_event *event);
        static void select_all(ltk_entry *entry, ltk_key_event *event);
        static void delete_char_backwards(ltk_entry *entry, ltk_key_event *event);
        static void delete_char_forwards(ltk_entry *entry, ltk_key_event *event);
       +static void edit_external(ltk_entry *entry, ltk_key_event *event);
        static void recalc_ideal_size(ltk_entry *entry);
        static void ensure_cursor_shown(ltk_entry *entry);
       -static void insert_text(ltk_entry *entry, char *text, size_t len);
       +static void insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor);
        
        struct key_cb {
                char *text;
       t@@ -94,6 +96,7 @@ static struct key_cb cb_map[] = {
                {"cursor-to-end", &cursor_to_end},
                {"delete-char-backwards", &delete_char_backwards},
                {"delete-char-forwards", &delete_char_forwards},
       +        {"edit-external", &edit_external},
                {"expand-selection-left", &expand_selection_left},
                {"expand-selection-right", &expand_selection_right},
                {"paste-clipboard", &paste_clipboard},
       t@@ -170,6 +173,7 @@ static struct ltk_widget_vtable vtable = {
                .motion_notify = &ltk_entry_motion_notify,
                .mouse_leave = &ltk_entry_mouse_leave,
                .mouse_enter = &ltk_entry_mouse_enter,
       +        .cmd_return = &ltk_entry_cmd_return,
                .change_state = NULL,
                .get_child_at_pos = NULL,
                .resize = NULL,
       t@@ -425,7 +429,7 @@ paste_primary(ltk_entry *entry, ltk_key_event *event) {
                (void)event;
                txtbuf *buf = ltk_clipboard_get_primary_text(entry->widget.window->clipboard);
                if (buf)
       -                insert_text(entry, buf->text, buf->len);
       +                insert_text(entry, buf->text, buf->len, 1);
        }
        
        static void
       t@@ -433,7 +437,7 @@ paste_clipboard(ltk_entry *entry, ltk_key_event *event) {
                (void)event;
                txtbuf *buf = ltk_clipboard_get_clipboard_text(entry->widget.window->clipboard);
                if (buf)
       -                insert_text(entry, buf->text, buf->len);
       +                insert_text(entry, buf->text, buf->len, 1);
        }
        
        static void
       t@@ -529,7 +533,7 @@ ensure_cursor_shown(ltk_entry *entry) {
        
        /* FIXME: maybe make this a regular key binding with wildcard text like in ledit? */
        static void
       -insert_text(ltk_entry *entry, char *text, size_t len) {
       +insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor) {
                size_t num = 0;
                /* FIXME: this is ugly and there are probably a lot of other
                   cases that need to be handled */
       t@@ -558,7 +562,8 @@ insert_text(ltk_entry *entry, char *text, size_t len) {
                        if (text[i] != '\n' && text[i] != '\r')
                                entry->text[j++] = text[i];
                }
       -        entry->pos += reallen;
       +        if (move_cursor)
       +                entry->pos += reallen;
                entry->text[entry->len] = '\0';
                ltk_text_line_set_text(entry->tl, entry->text, 0);
                recalc_ideal_size(entry);
       t@@ -567,6 +572,29 @@ insert_text(ltk_entry *entry, char *text, size_t len) {
                ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
        }
        
       +static void
       +ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len) {
       +        ltk_entry *e = (ltk_entry *)self;
       +        wipe_selection(e);
       +        e->len = e->pos = 0;
       +        insert_text(e, text, len, 0);
       +}
       +
       +static void
       +edit_external(ltk_entry *entry, ltk_key_event *event) {
       +        (void)event;
       +        ltk_config *config = ltk_config_get();
       +        /* FIXME: allow arguments to key mappings - this would allow to have different key mappings
       +           for different editors instead of just one command */
       +        if (!config->general.line_editor) {
       +                ltk_warn("Unable to run external editing command: line editor not configured\n");
       +        } else {
       +                /* FIXME: somehow show that there was an error if this returns 1? */
       +                /* FIXME: change interface to not require length of cmd */
       +                ltk_window_call_cmd(entry->widget.window, &entry->widget, config->general.line_editor, strlen(config->general.line_editor), entry->text, entry->len);
       +        }
       +}
       +
        static int
        ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) {
                ltk_entry *entry = (ltk_entry *)self;
       t@@ -590,7 +618,7 @@ ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) {
                        /* FIXME: properly handle everything */
                        if (event->text[0] == '\n' || event->text[0] == '\r' || event->text[0] == 0x1b)
                                return 0;
       -                insert_text(entry, event->text, strlen(event->text));
       +                insert_text(entry, event->text, strlen(event->text), 1);
                        return 1;
                }
                return 0;
       t@@ -751,6 +779,7 @@ ltk_entry_destroy(ltk_widget *self, int shallow) {
                        ltk_warn("Tried to destroy NULL entry.\n");
                        return;
                }
       +        ltk_free(entry->text);
                ltk_surface_cache_release_key(entry->key);
                ltk_text_line_destroy(entry->tl);
                ltk_free(entry);
   DIR diff --git a/src/ltk.h b/src/ltk.h
       t@@ -59,6 +59,14 @@ struct ltk_window {
                ltk_widget *active_widget;
                ltk_widget *pressed_widget;
                void (*other_event) (struct ltk_window *, ltk_event *event);
       +
       +        /* 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. */
       +        int cmd_pid;
       +        char *cmd_tmpfile;
       +        char *cmd_caller;
       +
                ltk_rect rect;
                ltk_window_theme *theme;
                ltk_rect dirty_rect;
       t@@ -83,6 +91,7 @@ struct ltk_window_theme {
                ltk_color bg;
        };
        
       +int ltk_window_call_cmd(ltk_window *window, ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen);
        void ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect);
        void ltk_queue_event(ltk_window *window, ltk_userevent_type type, const char *id, const char *data);
        void ltk_window_set_hover_widget(ltk_window *window, ltk_widget *widget, ltk_motion_event *event);
   DIR diff --git a/src/ltkd.c b/src/ltkd.c
       t@@ -4,7 +4,7 @@
        /* FIXME: parsing doesn't work properly with bs? */
        /* FIXME: strip whitespace at end of lines in socket format */
        /*
       - * Copyright (c) 2016, 2017, 2018, 2020, 2021, 2022 lumidify <nobody@lumidify.org>
       + * Copyright (c) 2016-2018, 2020-2023 lumidify <nobody@lumidify.org>
         *
         * Permission to use, copy, modify, and/or distribute this software for any
         * purpose with or without fee is hereby granted, provided that the above
       t@@ -34,6 +34,8 @@
        
        #include <sys/un.h>
        #include <sys/stat.h>
       +#include <sys/wait.h>
       +#include <sys/types.h>
        #include <sys/select.h>
        #include <sys/socket.h>
        
       t@@ -441,7 +443,34 @@ ltk_mainloop(ltk_window *window) {
                ltk_generate_keyboard_event(window->renderdata, &event);
                ltk_handle_event(window, &event);
        
       +        int pid = -1;
       +        int wstatus = 0;
                while (running) {
       +                if (window->cmd_caller && (pid = waitpid(window->cmd_pid, &wstatus, WNOHANG)) > 0) {
       +                        ltk_error err;
       +                        ltk_widget *cmd_caller = ltk_get_widget(window->cmd_caller, LTK_WIDGET_ANY, &err);
       +                        /* FIXME: should commands be split into read/write and block write commands during external editing? */
       +                        /* FIXME: what if a new widget with same id was created in meantime? */
       +                        if (!cmd_caller) {
       +                                ltk_warn("Widget '%s' disappeared while text was being edited in external program\n", window->cmd_caller);
       +                        } else if (cmd_caller->vtable->cmd_return) {
       +                                size_t file_len = 0;
       +                                char *errstr = NULL;
       +                                char *contents = ltk_read_file(window->cmd_tmpfile, &file_len, &errstr);
       +                                if (!contents) {
       +                                        ltk_warn("Unable to read file '%s' written by external command: %s\n", window->cmd_tmpfile, errstr);
       +                                } else {
       +                                        cmd_caller->vtable->cmd_return(cmd_caller, contents, file_len);
       +                                        ltk_free(contents);
       +                                }
       +                        }
       +                        ltk_free(window->cmd_caller);
       +                        window->cmd_caller = NULL;
       +                        window->cmd_pid = -1;
       +                        unlink(window->cmd_tmpfile);
       +                        ltk_free(window->cmd_tmpfile);
       +                        window->cmd_tmpfile = NULL;
       +                }
                        rfds = sock_state.rallfds;
                        wfds = sock_state.wallfds;
                        /* separate these because the writing fds are usually
       t@@ -1030,6 +1059,40 @@ handle_keyrelease_binding(const char *widget_name, size_t wlen, const char *name
                return 1;
        }
        
       +int
       +ltk_window_call_cmd(ltk_window *window, ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen) {
       +        if (window->cmd_caller) {
       +                /* FIXME: allow multiple programs? */
       +                ltk_warn("External program to edit text is already being run\n");
       +                return 1;
       +        }
       +        /* FIXME: support environment variable $TMPDIR */
       +        ltk_free(window->cmd_tmpfile);
       +        window->cmd_tmpfile = ltk_strdup("/tmp/ltk.XXXXXX");
       +        int fd = mkstemp(window->cmd_tmpfile);
       +        if (fd == -1) {
       +                ltk_warn("Unable to create temporary file while trying to run command '%.*s'\n", (int)cmdlen, cmd);
       +                return 1;
       +        }
       +        close(fd);
       +        /* FIXME: give file descriptor directly to modified version of ltk_write_file */
       +        char *errstr = NULL;
       +        if (ltk_write_file(window->cmd_tmpfile, text, textlen, &errstr)) {
       +                ltk_warn("Unable to write to file '%s' while trying to run command '%.*s': %s\n", window->cmd_tmpfile, (int)cmdlen, cmd, errstr);
       +                unlink(window->cmd_tmpfile);
       +                return 1;
       +        }
       +        int pid = -1;
       +        if ((pid = ltk_parse_run_cmd(cmd, cmdlen, window->cmd_tmpfile)) <= 0) {
       +                ltk_warn("Unable to run command '%.*s'\n", (int)cmdlen, cmd);
       +                unlink(window->cmd_tmpfile);
       +                return 1;
       +        }
       +        window->cmd_pid = pid;
       +        window->cmd_caller = ltk_strdup(caller->id);
       +        return 0;
       +}
       +
        static ltk_window *
        ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int h) {
                char *theme_path;
       t@@ -1070,6 +1133,10 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
                window->active_widget = NULL;
                window->pressed_widget = NULL;
        
       +        window->cmd_pid = -1;
       +        window->cmd_tmpfile = NULL;
       +        window->cmd_caller = NULL;
       +
                window->surface_cache = ltk_surface_cache_create(window->renderdata);
        
                window->other_event = &ltk_window_other_event;
       t@@ -1092,6 +1159,7 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
        
        static void
        ltk_destroy_window(ltk_window *window) {
       +        ltk_free(window->cmd_tmpfile);
                ltk_clipboard_destroy(window->clipboard);
                ltk_text_context_destroy(window->text_context);
                if (window->popups)
   DIR diff --git a/src/text_pango.c b/src/text_pango.c
       t@@ -166,6 +166,8 @@ ltk_text_line_add_attr_bg(ltk_text_line *tl, size_t start, size_t end, ltk_color
                PangoAttribute *attr = pango_attr_background_new(c.red, c.green, c.blue);
                attr->start_index = start;
                attr->end_index = end;
       +        /* FIXME: this is sketchy - if add_attr_bg/fg is called multiple times,
       +           pango_layout_set_attributes will probably ref the same AttrList multiple times */
                pango_attr_list_insert(tl->attrs, attr);
                pango_layout_set_attributes(tl->layout, tl->attrs);
        }
       t@@ -355,7 +357,7 @@ ltk_text_line_move_cursor_visually(ltk_text_line *tl, size_t pos, int movement, 
        void
        ltk_text_line_destroy(ltk_text_line *tl) {
                if (tl->attrs)
       -                g_object_unref(tl->attrs);
       +                pango_attr_list_unref(tl->attrs);
                g_object_unref(tl->layout);
                ltk_free(tl->text);
                ltk_free(tl);
   DIR diff --git a/src/text_stb.c b/src/text_stb.c
       t@@ -331,9 +331,10 @@ static ltk_font *
        ltk_create_font(char *path, uint16_t id, int index) {
                unsigned long len;
                ltk_font *font = ltk_malloc(sizeof(ltk_font));
       -        char *contents = ltk_read_file(path, &len);
       +        char *errstr = NULL;
       +        char *contents = ltk_read_file(path, &len, &errstr);
                if (!contents)
       -                ltk_fatal_errno("Unable to read font file %s\n", path);
       +                ltk_fatal_errno("Unable to read font file %s: %s\n", path, errstr);
                int offset = stbtt_GetFontOffsetForIndex((unsigned char *)contents, index);
                font->info.data = NULL;
                if (!stbtt_InitFont(&font->info, (unsigned char *)contents, offset))
   DIR diff --git a/src/theme.c b/src/theme.c
       t@@ -46,6 +46,7 @@ ltk_theme_handle_value(ltk_window *window, char *debug_name, const char *prop, c
                        }
                        break;
                case THEME_STRING:
       +                /* FIXME: check if already set? */
                        *(entry->ptr.str) = ltk_strdup(value);
                        entry->initialized = 1;
                        break;
       t@@ -102,6 +103,8 @@ int
        ltk_theme_fill_defaults(ltk_window *window, char *debug_name, ltk_theme_parseinfo *parseinfo, size_t len) {
                for (size_t i = 0; i < len; i++) {
                        ltk_theme_parseinfo *e = &parseinfo[i];
       +                if (e->initialized)
       +                        continue;
                        switch (e->type) {
                        case THEME_INT:
                                *(e->ptr.i) = e->defaultval.i;
       t@@ -143,7 +146,7 @@ ltk_theme_uninitialize(ltk_window *window, ltk_theme_parseinfo *parseinfo, size_
                                continue;
                        switch (e->type) {
                        case THEME_STRING:
       -                        free(*(e->ptr.str));
       +                        ltk_free(*(e->ptr.str));
                                e->initialized = 0;
                                break;
                        case THEME_COLOR:
   DIR diff --git a/src/txtbuf.c b/src/txtbuf.c
       t@@ -17,7 +17,7 @@ txtbuf_new(void) {
        }
        
        txtbuf *
       -txtbuf_new_from_char(char *str) {
       +txtbuf_new_from_char(const char *str) {
                txtbuf *buf = ltk_malloc(sizeof(txtbuf));
                buf->text = ltk_strdup(str);
                buf->len = strlen(str);
       t@@ -26,7 +26,7 @@ txtbuf_new_from_char(char *str) {
        }
        
        txtbuf *
       -txtbuf_new_from_char_len(char *str, size_t len) {
       +txtbuf_new_from_char_len(const char *str, size_t len) {
                txtbuf *buf = ltk_malloc(sizeof(txtbuf));
                buf->text = ltk_strndup(str, len);
                buf->len = len;
       t@@ -35,7 +35,7 @@ txtbuf_new_from_char_len(char *str, size_t len) {
        }
        
        void
       -txtbuf_fmt(txtbuf *buf, char *fmt, ...) {
       +txtbuf_fmt(txtbuf *buf, const char *fmt, ...) {
                va_list args;
                va_start(args, fmt);
                int len = vsnprintf(buf->text, buf->cap, fmt, args);
       t@@ -52,12 +52,12 @@ txtbuf_fmt(txtbuf *buf, char *fmt, ...) {
        }
        
        void
       -txtbuf_set_text(txtbuf *buf, char *text) {
       +txtbuf_set_text(txtbuf *buf, const char *text) {
                txtbuf_set_textn(buf, text, strlen(text));
        }
        
        void
       -txtbuf_set_textn(txtbuf *buf, char *text, size_t len) {
       +txtbuf_set_textn(txtbuf *buf, const char *text, size_t len) {
                txtbuf_resize(buf, len);
                buf->len = len;
                memmove(buf->text, text, len);
       t@@ -65,7 +65,7 @@ txtbuf_set_textn(txtbuf *buf, char *text, size_t len) {
        }
        
        void
       -txtbuf_append(txtbuf *buf, char *text) {
       +txtbuf_append(txtbuf *buf, const char *text) {
                txtbuf_appendn(buf, text, strlen(text));
        }
        
       t@@ -73,7 +73,7 @@ txtbuf_append(txtbuf *buf, char *text) {
           space so a buffer that will be filled up anyways doesn't have to be
           constantly resized */
        void
       -txtbuf_appendn(txtbuf *buf, char *text, size_t len) {
       +txtbuf_appendn(txtbuf *buf, const char *text, size_t len) {
                /* FIXME: overflow protection here and everywhere else */
                txtbuf_resize(buf, buf->len + len);
                memmove(buf->text + buf->len, text, len);
       t@@ -116,6 +116,11 @@ txtbuf_dup(txtbuf *src) {
                return dst;
        }
        
       +char *
       +txtbuf_get_textcopy(txtbuf *buf) {
       +        return buf->text ? ltk_strndup(buf->text, buf->len) : ltk_strdup("");
       +}
       +
        /* FIXME: proper "normalize" function to add nul-termination if needed */
        int
        txtbuf_cmp(txtbuf *buf1, txtbuf *buf2) {
   DIR diff --git a/src/txtbuf.h b/src/txtbuf.h
       t@@ -24,39 +24,39 @@ txtbuf *txtbuf_new(void);
         * Create a new txtbuf, initializing it with the nul-terminated
         * string 'str'. The input string is copied.
         */
       -txtbuf *txtbuf_new_from_char(char *str);
       +txtbuf *txtbuf_new_from_char(const char *str);
        
        /*
         * Create a new txtbuf, initializing it with the string 'str'
         * of length 'len'. The input string is copied.
         */
       -txtbuf *txtbuf_new_from_char_len(char *str, size_t len);
       +txtbuf *txtbuf_new_from_char_len(const char *str, size_t len);
        
        /*
         * Replace the stored text in 'buf' with the text generated by
         * 'snprintf' when called with the given format string and args.
         */
       -void txtbuf_fmt(txtbuf *buf, char *fmt, ...);
       +void txtbuf_fmt(txtbuf *buf, const char *fmt, ...);
        
        /*
         * Replace the stored text in 'buf' with 'text'.
         */
       -void txtbuf_set_text(txtbuf *buf, char *text);
       +void txtbuf_set_text(txtbuf *buf, const char *text);
        
        /*
         * Same as txtbuf_set_text, but with explicit length for 'text'.
         */
       -void txtbuf_set_textn(txtbuf *buf, char *text, size_t len);
       +void txtbuf_set_textn(txtbuf *buf, const char *text, size_t len);
        
        /*
         * Append 'text' to the text stored in 'buf'.
         */
       -void txtbuf_append(txtbuf *buf, char *text);
       +void txtbuf_append(txtbuf *buf, const char *text);
        
        /*
         * Same as txtbuf_append, but with explicit length for 'text'.
         */
       -void txtbuf_appendn(txtbuf *buf, char *text, size_t len);
       +void txtbuf_appendn(txtbuf *buf, const char *text, size_t len);
        
        /*
         * Compare the text of two txtbuf's like 'strcmp'.
       t@@ -91,6 +91,12 @@ void txtbuf_copy(txtbuf *dst, txtbuf *src);
        txtbuf *txtbuf_dup(txtbuf *src);
        
        /*
       + * Get copy of text stored in 'buf'.
       + * The returned text belongs to the caller and needs to be freed.
       + */
       +char *txtbuf_get_textcopy(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/util.c b/src/util.c
       t@@ -1,5 +1,5 @@
        /*
       - * Copyright (c) 2021 lumidify <nobody@lumidify.org>
       + * Copyright (c) 2021, 2023 lumidify <nobody@lumidify.org>
         *
         * Permission to use, copy, modify, and/or distribute this software for any
         * purpose with or without fee is hereby granted, provided that the above
       t@@ -15,6 +15,7 @@
         */
        
        #include <pwd.h>
       +#include <ctype.h>
        #include <errno.h>
        #include <stdio.h>
        #include <stdlib.h>
       t@@ -24,25 +25,250 @@
        #include <sys/stat.h>
        
        #include "util.h"
       +#include "array.h"
        #include "memory.h"
       +#include "txtbuf.h"
        
        /* FIXME: Should these functions really fail on memory error? */
       -/* FIXME: *len should be long, not unsigned long! */
       +
        char *
       -ltk_read_file(const char *path, unsigned long *len) {
       -        FILE *f;
       +ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret) {
       +        long len;
                char *file_contents;
       -        f = fopen(path, "rb");
       -        if (!f) return NULL;
       -        fseek(f, 0, SEEK_END);
       -        *len = ftell(f);
       -        fseek(f, 0, SEEK_SET);
       -        file_contents = ltk_malloc(*len + 1);
       -        fread(file_contents, 1, *len, f);
       -        file_contents[*len] = '\0';
       -        fclose(f);
       +        FILE *file;
        
       +        /* FIXME: https://wiki.sei.cmu.edu/confluence/display/c/FIO19-C.+Do+not+use+fseek()+and+ftell()+to+compute+the+size+of+a+regular+file */
       +        file = fopen(filename, "r");
       +        if (!file) goto error;
       +        if (fseek(file, 0, SEEK_END)) goto errorclose;
       +        len = ftell(file);
       +        if (len < 0) goto errorclose;
       +        if (fseek(file, 0, SEEK_SET)) goto errorclose;
       +        file_contents = ltk_malloc((size_t)len + 1);
       +        clearerr(file);
       +        fread(file_contents, 1, (size_t)len, file);
       +        if (ferror(file)) goto errorclose;
       +        file_contents[len] = '\0';
       +        if (fclose(file)) goto error;
       +        *len_ret = (size_t)len;
                return file_contents;
       +error:
       +        if (errstr_ret)
       +                *errstr_ret = strerror(errno);
       +        return NULL;
       +errorclose:
       +        if (errstr_ret)
       +                *errstr_ret = strerror(errno);
       +        fclose(file);
       +        return NULL;
       +}
       +
       +/* FIXME: not sure if errno actually is set usefully after all these functions */
       +int
       +ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret) {
       +        FILE *file = fopen(path, "w");
       +        if (!file) goto error;
       +        clearerr(file);
       +        if (fwrite(data, 1, len, file) < len) goto errorclose;
       +        if (fclose(file)) goto error;
       +        return 0;
       +error:
       +        if (errstr_ret)
       +                *errstr_ret = strerror(errno);
       +        return 1;
       +errorclose:
       +        if (errstr_ret)
       +                *errstr_ret = strerror(errno);
       +        fclose(file);
       +        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
   DIR diff --git a/src/util.h b/src/util.h
       t@@ -1,5 +1,5 @@
        /*
       - * Copyright (c) 2021 lumidify <nobody@lumidify.org>
       + * Copyright (c) 2021, 2023 lumidify <nobody@lumidify.org>
         *
         * Permission to use, copy, modify, and/or distribute this software for any
         * purpose with or without fee is hereby granted, provided that the above
       t@@ -25,7 +25,9 @@ long long ltk_strtonum(
            long long maxval, const char **errstrp
        );
        
       -char *ltk_read_file(const char *path, unsigned long *len);
       +char *ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret);
       +int ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret);
       +int ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename);
        void ltk_grow_string(char **str, int *alloc_size, int needed);
        char *ltk_setup_directory(void);
        char *ltk_strcat_useful(const char *str1, const char *str2);
   DIR diff --git a/src/widget.h b/src/widget.h
       t@@ -128,6 +128,7 @@ struct ltk_widget_vtable {
                int (*mouse_enter)(struct ltk_widget *, ltk_motion_event *);
                int (*press)(struct ltk_widget *);
                int (*release)(struct ltk_widget *);
       +        void (*cmd_return)(struct ltk_widget *self, char *text, size_t len);
        
                void (*resize)(struct ltk_widget *);
                void (*hide)(struct ltk_widget *);