ltk

GUI toolkit for X11 (WIP)
git clone git://lumidify.org/ltk.git (fast, but not encrypted)
git clone https://lumidify.org/ltk.git (encrypted, but very slow)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ltk.git (over tor)
Log | Files | Refs | README | LICENSE

ltk.c (22755B)


      1 /*
      2  * Copyright (c) 2016-2026 lumidify <nobody@lumidify.org>
      3  *
      4  * Permission to use, copy, modify, and/or distribute this software for any
      5  * purpose with or without fee is hereby granted, provided that the above
      6  * copyright notice and this permission notice appear in all copies.
      7  *
      8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     15  */
     16 
     17 #include <locale.h>
     18 #include <pwd.h>
     19 #include <stdint.h>
     20 #include <stdlib.h>
     21 #include <string.h>
     22 #include <time.h>
     23 #include <unistd.h>
     24 
     25 #include <sys/wait.h>
     26 
     27 #include "ltk.h"
     28 #include "array.h"
     29 #include "button.h"
     30 #include "config.h"
     31 #include "entry.h"
     32 #include "event.h"
     33 #include "eventdefs.h"
     34 #include "graphics.h"
     35 #include "image.h"
     36 #include "label.h"
     37 #include "macros.h"
     38 #include "memory.h"
     39 #include "menu.h"
     40 #include "rect.h"
     41 #include "scrollbar.h"
     42 #include "text.h"
     43 #include "util.h"
     44 #include "widget.h"
     45 #include "widget_internal.h"
     46 
     47 typedef struct {
     48 	ltk_widget_id caller;
     49 	char *infile;
     50 	char *outfile;
     51 	int pid;
     52 } ltk_cmdinfo;
     53 
     54 typedef struct {
     55 	void (*callback)(ltk_callback_arg data);
     56 	ltk_callback_arg data;
     57 	struct timespec repeat;
     58 	struct timespec remaining;
     59 	int id;
     60 } ltk_timer;
     61 
     62 LTK_ARRAY_INIT_DECL_STATIC(window, ltk_window *)
     63 LTK_ARRAY_INIT_IMPL_STATIC(window, ltk_window *)
     64 LTK_ARRAY_INIT_DECL_STATIC(rwindow, ltk_renderwindow *)
     65 LTK_ARRAY_INIT_IMPL_STATIC(rwindow, ltk_renderwindow *)
     66 LTK_ARRAY_INIT_DECL_STATIC(cmdinfo, ltk_cmdinfo)
     67 LTK_ARRAY_INIT_IMPL_STATIC(cmdinfo, ltk_cmdinfo)
     68 LTK_ARRAY_INIT_DECL_STATIC(widget, ltk_widget *)
     69 LTK_ARRAY_INIT_IMPL_STATIC(widget, ltk_widget *)
     70 LTK_ARRAY_INIT_DECL_STATIC(gen_num, ltk_widget_id_int_type)
     71 LTK_ARRAY_INIT_IMPL_STATIC(gen_num, ltk_widget_id_int_type)
     72 LTK_ARRAY_INIT_DECL_STATIC(timer, ltk_timer)
     73 LTK_ARRAY_INIT_IMPL_STATIC(timer, ltk_timer)
     74 
     75 static struct {
     76 	ltk_renderdata *renderdata;
     77 	ltk_text_context *text_context;
     78 	ltk_clipboard *clipboard;
     79 	/* FIXME: For the current event mechanism to work properly, the windows need to be stored
     80 	   in a separate array here in addition to the general widgets array that they are stored
     81 	   in anyways. Maybe fix this so they are only stored in one place? */
     82 	ltk_array(window) *windows;
     83 	ltk_array(rwindow) *rwindows;
     84 	/* PID of external command called e.g. by text widget to edit text.
     85 	   ON exit, cmd_caller->vtable->cmd_return is called with the text
     86 	   the external command wrote to a file. */
     87 	/*FIXME: this needs to be checked whenever a widget is destroyed!*/
     88 	/* -> actually, if widget ID system is implemented, this can just check if ID is still valid on command return
     89 	      -> although, that could cause weirdness in extreme edge cases when the generation number of the ID has
     90 		 wrapped around so there is actually a valid ID at that position - would at least need to check if cmd_return is actually valid for widget */
     91 	ltk_array(cmdinfo) *cmds;
     92 	ltk_array(timer) *timers;
     93 	/* widgets is an array of all widgets, with entries of deleted widgets being NULL until
     94 	   they are reused. cur_widget_clock_pos contains the index from which a search for an
     95 	   empty slot should start. If no empty slot is found and the size of the array is less
     96 	   than LTK_MAX_WIDGET_IDX, the array size is increased. In order to give an error when
     97 	   a widget that has been deleted is requested and the slot has already been filled with
     98 	   a new widget, each widget ID contains a generation number. gen_nums contains the
     99 	   current generation number for each slot, which is increased whenever a widget is added
    100 	   to the slot. A valid widget ID can never have a generation number of 0 (this is so
    101 	   there's a special widget ID that can be used to signal that the ID is invalid). */
    102 	ltk_array(widget) *widgets;
    103 	ltk_array(gen_num) *gen_nums;
    104 	ltk_widget_id_int_type cur_widget_clock_pos;
    105 	size_t cur_kbd;
    106 } shared_data = {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0};
    107 
    108 static void ltk_handle_event(ltk_event *event);
    109 
    110 static short running = 1;
    111 
    112 typedef struct {
    113 	char *name;
    114 	void (*cleanup)(void);
    115 } ltk_widget_funcs;
    116 
    117 /* FIXME: I guess the names aren't needed anymore here, but who
    118    knows if I'll need them again sometime... */
    119 static ltk_widget_funcs widget_funcs[] = {
    120 	{
    121 		.name = "entry",
    122 		.cleanup = &ltk_entry_cleanup,
    123 	},
    124 	{
    125 		.name = "combobox",
    126 		.cleanup = &ltk_combobox_cleanup,
    127 	},
    128 	{
    129 		/* Handler for window theme. */
    130 		.name = "window",
    131 		.cleanup = &ltk_window_cleanup,
    132 	}
    133 };
    134 
    135 ltk_renderdata *
    136 ltk_get_renderer(void) {
    137 	/* FIXME: check if initialized? */
    138 	return shared_data.renderdata;
    139 }
    140 
    141 int
    142 ltk_init(void) {
    143 	/* FIXME: should ltk set this? probably not */
    144 	setlocale(LC_CTYPE, "");
    145 	char *ltk_dir = ltk_setup_directory("LTKDIR", ".ltk", 0);
    146 	if (!ltk_dir)
    147 		ltk_fatal_errno("Unable to setup ltk directory.\n");
    148 	shared_data.cur_kbd = 0;
    149 
    150 	shared_data.renderdata = ltk_renderer_create();
    151 	if (!shared_data.renderdata)
    152 		return 1; /* FIXME: clean up */
    153 
    154 	/* FIXME: search different directories for config */
    155 	/* FIXME: don't print error if config file doesn't exist */
    156 	char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg");
    157 	ltk_free0(ltk_dir);
    158 	char *errstr = NULL;
    159 	if (ltk_config_parsefile(shared_data.renderdata, config_path, &errstr)) {
    160 		if (errstr) {
    161 			ltk_warn("Unable to load config: %s\n", errstr);
    162 			ltk_free0(errstr);
    163 		}
    164 		if (ltk_config_load_default(shared_data.renderdata, &errstr)) {
    165 			/* FIXME: I guess errstr isn't freed here, but whatever */
    166 			/* FIXME: return error instead of dying */
    167 			ltk_fatal("Unable to load default config: %s\n", errstr);
    168 		}
    169 	}
    170 	ltk_free0(config_path);
    171 
    172 	ltk_events_init(shared_data.renderdata);
    173 	shared_data.text_context = ltk_text_context_create(shared_data.renderdata);
    174 	shared_data.clipboard = ltk_clipboard_create(shared_data.renderdata);
    175 	/* FIXME: configure cache size; check for overflow */
    176 	ltk_image_init(shared_data.renderdata, 1024 * 1024 * 4);
    177 	shared_data.windows = ltk_array_create(window, 1);
    178 	shared_data.rwindows = ltk_array_create(rwindow, 1);
    179 	shared_data.cmds = ltk_array_create(cmdinfo, 1);
    180 	shared_data.widgets = ltk_array_create(widget, 1);
    181 	shared_data.gen_nums = ltk_array_create(gen_num, 1);
    182 	shared_data.timers = ltk_array_create(timer, 1);
    183 	return 0; /* FIXME: or maybe 1? */
    184 }
    185 
    186 ltk_widget *
    187 ltk_get_widget_from_id(ltk_widget_id id) {
    188 	ltk_assert(id.gen > 0);
    189 	ltk_assert(id.idx < ltk_array_len(shared_data.widgets));
    190 	ltk_widget *widget = ltk_array_get(shared_data.widgets, id.idx);
    191 	ltk_assert(widget != NULL);
    192 	ltk_assert(widget->id.gen == id.gen);
    193 	return widget;
    194 }
    195 
    196 ltk_widget *
    197 ltk_get_widget_or_null_from_id(ltk_widget_id id) {
    198 	return id.gen == 0 ? NULL : ltk_get_widget_from_id(id);
    199 }
    200 
    201 ltk_widget *
    202 ltk_get_widget_or_null_from_id_nofail(ltk_widget_id id) {
    203 	if (id.gen == 0 || id.idx >= ltk_array_len(shared_data.widgets))
    204 		return NULL;
    205 	ltk_widget *widget = ltk_array_get(shared_data.widgets, id.idx);
    206 	if (!widget || widget->id.gen != id.gen)
    207 		return NULL;
    208 	return widget;
    209 }
    210 
    211 ltk_widget_id
    212 ltk_store_widget(ltk_widget *widget) {
    213 	for (size_t i = 0; i < ltk_array_len(shared_data.widgets); ++i) {
    214 		if (!ltk_array_get(shared_data.widgets, shared_data.cur_widget_clock_pos)) {
    215 			size_t idx = shared_data.cur_widget_clock_pos;
    216 			ltk_array_get(shared_data.widgets, idx) = widget;
    217 			ltk_array_get(shared_data.gen_nums, idx)++;
    218 			shared_data.cur_widget_clock_pos++;
    219 			shared_data.cur_widget_clock_pos %= ltk_array_len(shared_data.widgets);
    220 			return (ltk_widget_id){idx, ltk_array_get(shared_data.gen_nums, idx)};
    221 		}
    222 		shared_data.cur_widget_clock_pos++;
    223 		shared_data.cur_widget_clock_pos %= ltk_array_len(shared_data.widgets);
    224 	}
    225 	size_t idx = ltk_array_len(shared_data.widgets);
    226 	ltk_assert(idx <= LTK_MAX_WIDGET_IDX);
    227 	ltk_array_append(widget, shared_data.widgets, widget);
    228 	ltk_array_len_to_capacity(widget, shared_data.widgets, NULL);
    229 	if (ltk_array_len(shared_data.widgets) > (size_t)LTK_MAX_WIDGET_IDX + 1U)
    230 		ltk_array_resize(widget, shared_data.widgets, (size_t)LTK_MAX_WIDGET_IDX + 1U, NULL);
    231 	ltk_array_resize(gen_num, shared_data.gen_nums, ltk_array_len(shared_data.widgets), 0);
    232 	ltk_array_get(shared_data.gen_nums, idx) = 1;
    233 	shared_data.cur_widget_clock_pos = idx + 1;
    234 	shared_data.cur_widget_clock_pos %= ltk_array_len(shared_data.widgets);
    235 	return (ltk_widget_id){idx, 1};
    236 }
    237 
    238 void
    239 ltk_clear_widget_slot(ltk_widget_id id) {
    240 	ltk_assert(id.idx < ltk_array_len(shared_data.widgets));
    241 	ltk_array_get(shared_data.widgets, id.idx) = NULL;
    242 }
    243 
    244 static struct {
    245 	struct timespec last;
    246 	struct timespec lasttimer;
    247 } mainloop_data;
    248 
    249 void
    250 ltk_mainloop_init(void) {
    251 	clock_gettime(CLOCK_MONOTONIC, &mainloop_data.last);
    252 	mainloop_data.lasttimer = mainloop_data.last;
    253 
    254 	/* initialize keyboard mapping */
    255 	ltk_event event;
    256 	ltk_generate_keyboard_event(shared_data.renderdata, &event);
    257 	ltk_handle_event(&event);
    258 }
    259 
    260 /* FIXME: maybe split this up into multiple stages */
    261 void
    262 ltk_mainloop_step(int limit_framerate) {
    263 	ltk_event event;
    264 
    265 	/* FIXME: make time management smarter - maybe always figure out how long
    266 	   it will take until the next timer is due and then sleep if no other events
    267 	   are happening (would need separate parameter to turn that off when a
    268 	   different mainloop is used) */
    269 	struct timespec now, elapsed, sleep_time;
    270 	sleep_time.tv_sec = 0;
    271 
    272 	int pid = -1;
    273 	int wstatus = 0;
    274 	/* FIXME: kill all children on exit? */
    275 	/* -> at least unlink any files? */
    276 	if ((pid = waitpid(-1, &wstatus, WNOHANG)) > 0) {
    277 		ltk_cmdinfo *info;
    278 		/* FIXME: should commands be split into read/write and block write commands during external editing? */
    279 		for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
    280 			info = &(ltk_array_get(shared_data.cmds, i));
    281 			if (info->pid == pid) {
    282 				ltk_widget *caller = ltk_get_widget_or_null_from_id_nofail(info->caller);
    283 				if (!caller) {
    284 					ltk_warn("Widget disappeared while text was being edited in external program\n");
    285 				/* FIXME: call overwritten cmd_return! */
    286 				} else if (caller->vtable->cmd_return) {
    287 					size_t file_len = 0;
    288 					char *errstr = NULL;
    289 					char *filename = info->outfile ? info->outfile : info->infile;
    290 					char *contents = ltk_read_file(filename, &file_len, &errstr);
    291 					if (!contents) {
    292 						ltk_warn("Unable to read file '%s' written by external command: %s\n", filename, errstr);
    293 					} else {
    294 						caller->vtable->cmd_return(caller, contents, file_len);
    295 						ltk_free0(contents);
    296 					}
    297 				}
    298 				/* FIXME: error checking */
    299 				unlink(info->infile);
    300 				ltk_free(info->infile);
    301 				if (info->outfile) {
    302 					unlink(info->outfile);
    303 					ltk_free(info->outfile);
    304 				}
    305 				ltk_array_delete(cmdinfo, shared_data.cmds, i, 1);
    306 				break;
    307 			}
    308 		}
    309 	}
    310 	while (!ltk_next_event(
    311 	    shared_data.renderdata,
    312 	    ltk_array_get_buf(shared_data.rwindows),
    313 	    ltk_array_len(shared_data.rwindows),
    314 	    shared_data.clipboard, shared_data.cur_kbd, &event)) {
    315 		ltk_handle_event(&event);
    316 	}
    317 
    318 	clock_gettime(CLOCK_MONOTONIC, &now);
    319 	ltk_timespecsub(&now, &mainloop_data.lasttimer, &elapsed);
    320 	/* Note: it should be safe to give the same pointer as the first and
    321 	   last argument, as long as ltk_timespecsub/add isn't changed incompatibly */
    322 	size_t i = 0;
    323 	while (i < ltk_array_len(shared_data.timers)) {
    324 		ltk_timer *cur_timer = &ltk_array_get(shared_data.timers, i);
    325 		ltk_timespecsub(&cur_timer->remaining, &elapsed, &cur_timer->remaining);
    326 		if (cur_timer->remaining.tv_sec < 0 ||
    327 		    (cur_timer->remaining.tv_sec == 0 && cur_timer->remaining.tv_nsec == 0)) {
    328 			cur_timer->callback(cur_timer->data);
    329 			if (cur_timer->repeat.tv_sec == 0 && cur_timer->repeat.tv_nsec == 0) {
    330 				/* remove timer because it has no repeat */
    331 				ltk_array_delete(timer, shared_data.timers, i, 1);
    332 			} else {
    333 				ltk_timespecadd(&cur_timer->remaining, &cur_timer->repeat, &cur_timer->remaining);
    334 				i++;
    335 			}
    336 		} else {
    337 			i++;
    338 		}
    339 	}
    340 	mainloop_data.lasttimer = now;
    341 
    342 	for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
    343 		ltk_window *window = ltk_array_get(shared_data.windows, i);
    344 		if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) {
    345 			ltk_widget_draw(LTK_CAST_WIDGET(window), NULL, 0, 0, (ltk_rect){0, 0, 0, 0});
    346 		}
    347 	}
    348 
    349 	if (limit_framerate) {
    350 		clock_gettime(CLOCK_MONOTONIC, &now);
    351 		ltk_timespecsub(&now, &mainloop_data.last, &elapsed);
    352 		/* FIXME: configure framerate */
    353 		if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
    354 			sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
    355 			nanosleep(&sleep_time, NULL);
    356 		}
    357 		mainloop_data.last = now;
    358 	}
    359 }
    360 
    361 void
    362 ltk_mainloop_quit(void) {
    363 	/* FIXME: maybe prevent other events from running? */
    364 	running = 0;
    365 }
    366 
    367 void
    368 ltk_mainloop_restartable(void) {
    369 	ltk_mainloop_init();
    370 	while (running) {
    371 		ltk_mainloop_step(1);
    372 	}
    373 }
    374 
    375 void
    376 ltk_mainloop(void) {
    377 	ltk_mainloop_restartable();
    378 	ltk_deinit();
    379 }
    380 
    381 void
    382 ltk_deinit(void) {
    383 	/* if renderdata is NULL, the other initialization can't have happened either */
    384 	if (running || !shared_data.renderdata)
    385 		return;
    386 	if (shared_data.cmds) {
    387 		for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
    388 			/* FIXME: maybe kill child processes? */
    389 			ltk_free((ltk_array_get(shared_data.cmds, i)).infile);
    390 			if (ltk_array_get(shared_data.cmds, i).outfile)
    391 				ltk_free((ltk_array_get(shared_data.cmds, i)).outfile);
    392 		}
    393 		ltk_array_destroy(cmdinfo, shared_data.cmds);
    394 	}
    395 	shared_data.cmds = NULL;
    396 	/* FIXME: maybe special destroy function for cleanup at end that doesn't emit signals, etc. */
    397 	for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
    398 		ltk_window *window = ltk_array_get(shared_data.windows, i);
    399 		ltk_widget_destroy(LTK_CAST_WIDGET(window), 1);
    400 	}
    401 	ltk_array_destroy(window, shared_data.windows);
    402 	shared_data.windows = NULL;
    403 	ltk_array_destroy(rwindow, shared_data.rwindows);
    404 	shared_data.rwindows = NULL;
    405 	for (size_t i = 0; i < ltk_array_len(shared_data.widgets); i++) {
    406 		/* FIXME: not sure about this warning - the idea is to do a deep destroy of the
    407 		   windows above, then warn if there are any widgets that were not destroyed
    408 		   (i.e. not referenced in a window), but I'm not sure how useful that is */
    409 		ltk_widget *widget = ltk_array_get(shared_data.widgets, i);
    410 		if (widget) {
    411 			ltk_warn("Widget not contained within window still stored at index %zu in widget array on exit.\n", i);
    412 			ltk_widget_destroy(widget, 0);
    413 		}
    414 	}
    415 	ltk_array_destroy(widget, shared_data.widgets);
    416 	shared_data.widgets = NULL;
    417 	ltk_array_destroy(gen_num, shared_data.gen_nums);
    418 	shared_data.gen_nums = NULL;
    419 	ltk_array_destroy(timer, shared_data.timers);
    420 	shared_data.timers = NULL;
    421 	for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
    422 		if (widget_funcs[i].cleanup)
    423 			widget_funcs[i].cleanup();
    424 	}
    425 	ltk_config_cleanup(shared_data.renderdata);
    426 	if (shared_data.text_context)
    427 		ltk_text_context_destroy(shared_data.text_context);
    428 	shared_data.text_context = NULL;
    429 	if (shared_data.clipboard)
    430 		ltk_clipboard_destroy(shared_data.clipboard);
    431 	shared_data.clipboard = NULL;
    432 	ltk_events_cleanup();
    433 	ltk_renderer_destroy(shared_data.renderdata);
    434 	shared_data.renderdata = NULL;
    435 }
    436 
    437 /* FIXME: check everywhere if initialized already */
    438 ltk_widget_id
    439 ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h) {
    440 	/* FIXME: more asserts, or maybe global "initialized" flag */
    441 	ltk_assert(shared_data.renderdata != NULL);
    442 	ltk_assert(shared_data.windows != NULL);
    443 	ltk_assert(shared_data.rwindows != NULL);
    444 	ltk_window *window = ltk_window_create_intern(shared_data.renderdata, title, x, y, w, h);
    445 	ltk_array_append(window, shared_data.windows, window);
    446 	ltk_array_append(rwindow, shared_data.rwindows, window->renderwindow);
    447 	return LTK_CAST_WIDGET(window)->id;
    448 }
    449 
    450 void
    451 ltk_window_destroy(ltk_widget *self, int shallow) {
    452 	/* FIXME: would it make sense to do something with 'shallow' here? */
    453 	(void)shallow;
    454 	ltk_window *window = LTK_CAST_WINDOW(self);
    455 	for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
    456 		if (ltk_array_get(shared_data.windows, i) == window) {
    457 			ltk_array_delete(window, shared_data.windows, i, 1);
    458 			ltk_array_delete(rwindow, shared_data.rwindows, i, 1);
    459 			break;
    460 		}
    461 	}
    462 	ltk_window_destroy_intern(window);
    463 }
    464 
    465 ltk_clipboard *
    466 ltk_get_clipboard(void) {
    467 	/* FIXME: what to do when not initialized? */
    468 	return shared_data.clipboard;
    469 }
    470 
    471 /* FIXME: optimize timer handling - maybe also a sort of priority queue */
    472 void
    473 ltk_unregister_timer(int timer_id) {
    474 	for (size_t i = 0; i < ltk_array_len(shared_data.timers); i++) {
    475 		if (ltk_array_get(shared_data.timers, i).id == timer_id) {
    476 			ltk_array_delete(timer, shared_data.timers, i, 1);
    477 			return;
    478 		}
    479 	}
    480 }
    481 
    482 /* repeat <= 0 means no repeat, first <= 0 means run as soon as possible */
    483 int
    484 ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg data), ltk_callback_arg data) {
    485 	if (first < 0)
    486 		first = 0;
    487 	if (repeat < 0)
    488 		repeat = 0;
    489 	/* FIXME: better finding of id */
    490 	/* FIXME: maybe store sorted by id */
    491 	int id = 0;
    492 	for (size_t i = 0; i < ltk_array_len(shared_data.timers); i++) {
    493 		if (ltk_array_get(shared_data.timers, i).id >= id)
    494 			id = ltk_array_get(shared_data.timers, i).id + 1;
    495 	}
    496 	ltk_timer t;
    497 	t.callback = callback;
    498 	t.data = data;
    499 	t.repeat.tv_sec = repeat / 1000;
    500 	t.repeat.tv_nsec = (repeat % 1000) * 1000;
    501 	t.remaining.tv_sec = first / 1000;
    502 	t.remaining.tv_nsec = (first % 1000) * 1000;
    503 	t.id = id;
    504 	ltk_array_append(timer, shared_data.timers, t);
    505 	return id;
    506 }
    507 
    508 LTK_ARRAY_INIT_DECL_STATIC(str, char *)
    509 LTK_ARRAY_INIT_IMPL_STATIC(str, char *)
    510 
    511 static void
    512 str_free_helper(char *elem) {
    513 	ltk_free(elem);
    514 }
    515 
    516 int
    517 ltk_call_cmd(ltk_widget_id callerid, ltk_array(cmd) *cmd, const char *text, size_t textlen) {
    518 	/* FIXME: maybe support stdin/stdout without temporary files by just piping directly */
    519 	/* FIXME: support environment variable $TMPDIR */
    520 	ltk_cmdinfo info = {
    521 		.caller = LTK_WIDGET_ID_NONE, .infile = NULL, .outfile = NULL, .pid = -1
    522 	};
    523 	ltk_array(str) *cmdstr = ltk_array_create(str, 4);
    524 	txtbuf *tmpbuf = txtbuf_new();
    525 	int needs_stdin = 1;
    526 	int needs_stdout = 1;
    527 
    528 	int infd = -1, outfd = -1;
    529 
    530 	info.infile = ltk_strdup("/tmp/ltk.XXXXXX");
    531 	infd = mkstemp(info.infile);
    532 	if (infd == -1) {
    533 		ltk_warn_errno("Unable to create temporary input file while trying to run command.");
    534 		ltk_free(info.infile);
    535 		info.infile = NULL; /* so it isn't unlinked below */
    536 		goto error;
    537 	}
    538 	/* FIXME: give file descriptor directly to modified version of ltk_write_file */
    539 	char *errstr = NULL;
    540 	if (ltk_write_file(info.infile, text, textlen, &errstr)) {
    541 		ltk_warn("Unable to write to temporary input file '%s' while trying to run command.", info.infile, errstr);
    542 		goto error;
    543 	}
    544 
    545 	for (size_t i = 0; i < ltk_array_len(cmd); i++) {
    546 		ltk_array(cmdpiece) *pa = ltk_array_get(cmd, i);
    547 		for (size_t j = 0; j < ltk_array_len(pa); j++) {
    548 			struct ltk_cmd_piece p = ltk_array_get(pa, j);
    549 			switch (p.type) {
    550 			case LTK_CMD_TEXT:
    551 				txtbuf_append(tmpbuf, p.text);
    552 				break;
    553 			case LTK_CMD_INOUT_FILE:
    554 				needs_stdout = 0;
    555 				/* fall through */
    556 			case LTK_CMD_INPUT_FILE:
    557 				needs_stdin = 0;
    558 				txtbuf_append(tmpbuf, info.infile);
    559 				break;
    560 			case LTK_CMD_OUTPUT_FILE:
    561 				needs_stdout = 0;
    562 				if (!info.outfile) {
    563 					info.outfile = ltk_strdup("/tmp/ltk.XXXXXX");
    564 					outfd = mkstemp(info.outfile);
    565 					if (outfd == -1) {
    566 						ltk_warn_errno("Unable to create temporary output file while trying to run command.");
    567 						ltk_free(info.outfile);
    568 						info.outfile = NULL; /* so it isn't unlinked below */
    569 						goto error;
    570 					}
    571 				}
    572 				txtbuf_append(tmpbuf, info.outfile);
    573 				break;
    574 			default:
    575 				ltk_warn("Invalid command piece type. This should not happen.");
    576 				goto error;
    577 			}
    578 		}
    579 		ltk_array_append(str, cmdstr, txtbuf_get_textcopy(tmpbuf));
    580 		txtbuf_clear(tmpbuf);
    581 	}
    582 	/* if no output file was specified, we still need to create it for stdout */
    583 	if (needs_stdout) {
    584 		info.outfile = ltk_strdup("/tmp/ltk.XXXXXX");
    585 		outfd = mkstemp(info.outfile);
    586 		if (outfd == -1) {
    587 			ltk_warn_errno("Unable to create temporary output file while trying to run command.");
    588 			ltk_free(info.outfile);
    589 			info.outfile = NULL; /* so it isn't unlinked below */
    590 			goto error;
    591 		}
    592 	}
    593 	ltk_array_append(str, cmdstr, NULL); /* necessary for execvp */
    594 	txtbuf_destroy(tmpbuf);
    595 	tmpbuf = NULL;
    596 
    597 	int fret = -1;
    598 	if ((fret = fork()) < 0) {
    599 		ltk_warn("Unable to fork\n");
    600 		goto error;
    601 	} else if (fret == 0) {
    602 		if (needs_stdin) {
    603 			if (dup2(infd, fileno(stdin)) == -1)
    604 				ltk_fatal("Unable to set up stdin in child process.");
    605 		}
    606 		if (needs_stdout) {
    607 			int fd = outfd == -1 ? infd : outfd;
    608 			if (dup2(fd, fileno(stdout)) == -1)
    609 				ltk_fatal("Unable to set up stdout in child process.");
    610 		}
    611 		if (execvp(cmdstr->buf[0], cmdstr->buf) == -1)
    612 			ltk_fatal("Unable to exec external command.");
    613 	}
    614 	ltk_array_destroy_deep(str, cmdstr, &str_free_helper);
    615 
    616 	info.pid = fret;
    617 	info.caller = callerid;
    618 	ltk_array_append(cmdinfo, shared_data.cmds, info);
    619 
    620 	if (infd != -1)
    621 		close(infd); /* FIXME: error checking also on close */
    622 	if (outfd != -1)
    623 		close(outfd);
    624 	return 0;
    625 error:
    626 	if (infd != -1)
    627 		close(infd); /* FIXME: error checking also on close and unlink */
    628 	if (outfd != -1)
    629 		close(outfd);
    630 	if (tmpbuf)
    631 		txtbuf_destroy(tmpbuf);
    632 	if (info.infile) {
    633 		unlink(info.infile);
    634 		ltk_free(info.infile);
    635 	}
    636 	if (info.outfile) {
    637 		unlink(info.outfile);
    638 		ltk_free(info.outfile);
    639 	}
    640 	ltk_array_destroy_deep(str, cmdstr, &str_free_helper);
    641 	return 1;
    642 }
    643 
    644 static void
    645 ltk_handle_event(ltk_event *event) {
    646 	size_t kbd_idx;
    647 	if (event->type == LTK_KEYBOARDCHANGE_EVENT) {
    648 		/* FIXME: emit event */
    649 		if (ltk_config_get_language_index(event->keyboard.new_kbd, &kbd_idx))
    650 			ltk_warn("No language mapping for language \"%s\".\n", event->keyboard.new_kbd);
    651 		else
    652 			shared_data.cur_kbd = kbd_idx;
    653 	} else {
    654 		if (event->any.window_id < ltk_array_len(shared_data.windows)) {
    655 			ltk_window *window = ltk_array_get(shared_data.windows, event->any.window_id);
    656 			ltk_window_handle_event(LTK_CAST_WIDGET(window)->id, event);
    657 		} else {
    658 			ltk_warn("Invalid window ID %zu in event.", event->any.window_id);
    659 		}
    660 	}
    661 }
    662 
    663 ltk_text_line *
    664 ltk_text_line_create_default(const char *font, int font_size, char *text, int take_over_text, int width) {
    665 	return ltk_text_line_create(shared_data.text_context, font, font_size, text, take_over_text, width);
    666 }
    667 
    668 ltk_text_line *
    669 ltk_text_line_create_const_text_default(const char *font, int font_size, const char *text, int width) {
    670 	return ltk_text_line_create_const_text(shared_data.text_context, font, font_size, text, width);
    671 }