ltk.c (17932B)
1 /* 2 * Copyright (c) 2016-2024 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 *caller; 49 char *infile; 50 char *outfile; 51 int pid; 52 } ltk_cmdinfo; 53 54 LTK_ARRAY_INIT_DECL_STATIC(window, ltk_window *) 55 LTK_ARRAY_INIT_IMPL_STATIC(window, ltk_window *) 56 LTK_ARRAY_INIT_DECL_STATIC(rwindow, ltk_renderwindow *) 57 LTK_ARRAY_INIT_IMPL_STATIC(rwindow, ltk_renderwindow *) 58 LTK_ARRAY_INIT_DECL_STATIC(cmdinfo, ltk_cmdinfo) 59 LTK_ARRAY_INIT_IMPL_STATIC(cmdinfo, ltk_cmdinfo) 60 61 static struct { 62 ltk_renderdata *renderdata; 63 ltk_text_context *text_context; 64 ltk_clipboard *clipboard; 65 ltk_array(window) *windows; 66 ltk_array(rwindow) *rwindows; 67 /* PID of external command called e.g. by text widget to edit text. 68 ON exit, cmd_caller->vtable->cmd_return is called with the text 69 the external command wrote to a file. */ 70 /*FIXME: this needs to be checked whenever a widget is destroyed!*/ 71 ltk_array(cmdinfo) *cmds; 72 size_t cur_kbd; 73 } shared_data = {NULL, NULL, NULL, NULL, NULL, NULL, 0}; 74 75 typedef struct { 76 void (*callback)(ltk_callback_arg data); 77 ltk_callback_arg data; 78 struct timespec repeat; 79 struct timespec remaining; 80 int id; 81 } ltk_timer; 82 83 static ltk_timer *timers = NULL; 84 static size_t timers_num = 0; 85 static size_t timers_alloc = 0; 86 87 static void ltk_handle_event(ltk_event *event); 88 89 static short running = 1; 90 91 typedef struct { 92 char *name; 93 void (*cleanup)(void); 94 } ltk_widget_funcs; 95 96 /* FIXME: I guess the names aren't needed anymore here, but who 97 knows if I'll need them again sometime... */ 98 static ltk_widget_funcs widget_funcs[] = { 99 { 100 .name = "entry", 101 .cleanup = <k_entry_cleanup, 102 }, 103 { 104 .name = "combobox", 105 .cleanup = <k_combobox_cleanup, 106 }, 107 { 108 /* Handler for window theme. */ 109 .name = "window", 110 .cleanup = <k_window_cleanup, 111 } 112 }; 113 114 ltk_renderdata * 115 ltk_get_renderer(void) { 116 /* FIXME: check if initialized? */ 117 return shared_data.renderdata; 118 } 119 120 int 121 ltk_init(void) { 122 /* FIXME: should ltk set this? probably not */ 123 setlocale(LC_CTYPE, ""); 124 char *ltk_dir = ltk_setup_directory("LTKDIR", ".ltk", 0); 125 if (!ltk_dir) 126 ltk_fatal_errno("Unable to setup ltk directory.\n"); 127 shared_data.cur_kbd = 0; 128 129 shared_data.renderdata = ltk_renderer_create(); 130 if (!shared_data.renderdata) 131 return 1; /* FIXME: clean up */ 132 133 /* FIXME: search different directories for config */ 134 /* FIXME: don't print error if config file doesn't exist */ 135 char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg"); 136 ltk_free0(ltk_dir); 137 char *errstr = NULL; 138 if (ltk_config_parsefile(shared_data.renderdata, config_path, &errstr)) { 139 if (errstr) { 140 ltk_warn("Unable to load config: %s\n", errstr); 141 ltk_free0(errstr); 142 } 143 if (ltk_config_load_default(shared_data.renderdata, &errstr)) { 144 /* FIXME: I guess errstr isn't freed here, but whatever */ 145 /* FIXME: return error instead of dying */ 146 ltk_fatal("Unable to load default config: %s\n", errstr); 147 } 148 } 149 ltk_free0(config_path); 150 151 ltk_events_init(shared_data.renderdata); 152 shared_data.text_context = ltk_text_context_create(shared_data.renderdata); 153 shared_data.clipboard = ltk_clipboard_create(shared_data.renderdata); 154 /* FIXME: configure cache size; check for overflow */ 155 ltk_image_init(shared_data.renderdata, 1024 * 1024 * 4); 156 shared_data.windows = ltk_array_create(window, 1); 157 shared_data.rwindows = ltk_array_create(rwindow, 1); 158 shared_data.cmds = ltk_array_create(cmdinfo, 1); 159 return 0; /* FIXME: or maybe 1? */ 160 } 161 162 static struct { 163 struct timespec last; 164 struct timespec lasttimer; 165 } mainloop_data; 166 167 void 168 ltk_mainloop_init(void) { 169 clock_gettime(CLOCK_MONOTONIC, &mainloop_data.last); 170 mainloop_data.lasttimer = mainloop_data.last; 171 172 /* initialize keyboard mapping */ 173 ltk_event event; 174 ltk_generate_keyboard_event(shared_data.renderdata, &event); 175 ltk_handle_event(&event); 176 } 177 178 /* FIXME: maybe split this up into multiple stages */ 179 void 180 ltk_mainloop_step(int limit_framerate) { 181 ltk_event event; 182 183 /* FIXME: make time management smarter - maybe always figure out how long 184 it will take until the next timer is due and then sleep if no other events 185 are happening (would need separate parameter to turn that off when a 186 different mainloop is used) */ 187 struct timespec now, elapsed, sleep_time; 188 sleep_time.tv_sec = 0; 189 190 int pid = -1; 191 int wstatus = 0; 192 /* FIXME: kill all children on exit? */ 193 /* -> at least unlink any files? */ 194 if ((pid = waitpid(-1, &wstatus, WNOHANG)) > 0) { 195 ltk_cmdinfo *info; 196 /* FIXME: should commands be split into read/write and block write commands during external editing? */ 197 for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) { 198 info = &(ltk_array_get(shared_data.cmds, i)); 199 if (info->pid == pid) { 200 /* FIXME: actually NULL this when widgets are destroyed */ 201 if (!info->caller) { 202 ltk_warn("Widget disappeared while text was being edited in external program\n"); 203 /* FIXME: call overwritten cmd_return! */ 204 } else if (info->caller->vtable->cmd_return) { 205 size_t file_len = 0; 206 char *errstr = NULL; 207 char *filename = info->outfile ? info->outfile : info->infile; 208 char *contents = ltk_read_file(filename, &file_len, &errstr); 209 if (!contents) { 210 ltk_warn("Unable to read file '%s' written by external command: %s\n", filename, errstr); 211 } else { 212 info->caller->vtable->cmd_return(info->caller, contents, file_len); 213 ltk_free0(contents); 214 } 215 } 216 /* FIXME: error checking */ 217 unlink(info->infile); 218 ltk_free(info->infile); 219 if (info->outfile) { 220 unlink(info->outfile); 221 ltk_free(info->outfile); 222 } 223 ltk_array_delete(cmdinfo, shared_data.cmds, i, 1); 224 break; 225 } 226 } 227 } 228 while (!ltk_next_event( 229 shared_data.renderdata, 230 ltk_array_get_buf(shared_data.rwindows), 231 ltk_array_len(shared_data.rwindows), 232 shared_data.clipboard, shared_data.cur_kbd, &event)) { 233 ltk_handle_event(&event); 234 } 235 236 clock_gettime(CLOCK_MONOTONIC, &now); 237 ltk_timespecsub(&now, &mainloop_data.lasttimer, &elapsed); 238 /* Note: it should be safe to give the same pointer as the first and 239 last argument, as long as ltk_timespecsub/add isn't changed incompatibly */ 240 size_t i = 0; 241 while (i < timers_num) { 242 ltk_timespecsub(&timers[i].remaining, &elapsed, &timers[i].remaining); 243 if (timers[i].remaining.tv_sec < 0 || 244 (timers[i].remaining.tv_sec == 0 && timers[i].remaining.tv_nsec == 0)) { 245 timers[i].callback(timers[i].data); 246 if (timers[i].repeat.tv_sec == 0 && timers[i].repeat.tv_nsec == 0) { 247 /* remove timer because it has no repeat */ 248 memmove(timers + i, timers + i + 1, sizeof(ltk_timer) * (timers_num - i - 1)); 249 } else { 250 ltk_timespecadd(&timers[i].remaining, &timers[i].repeat, &timers[i].remaining); 251 i++; 252 } 253 } else { 254 i++; 255 } 256 } 257 mainloop_data.lasttimer = now; 258 259 for (size_t i = 0; i < shared_data.windows->len; i++) { 260 ltk_window *window = shared_data.windows->buf[i]; 261 if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) { 262 ltk_widget_draw(LTK_CAST_WIDGET(window), NULL, 0, 0, (ltk_rect){0, 0, 0, 0}); 263 } 264 } 265 266 if (limit_framerate) { 267 clock_gettime(CLOCK_MONOTONIC, &now); 268 ltk_timespecsub(&now, &mainloop_data.last, &elapsed); 269 /* FIXME: configure framerate */ 270 if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) { 271 sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec; 272 nanosleep(&sleep_time, NULL); 273 } 274 mainloop_data.last = now; 275 } 276 } 277 278 void 279 ltk_mainloop_quit(void) { 280 /* FIXME: maybe prevent other events from running? */ 281 running = 0; 282 } 283 284 void 285 ltk_mainloop_restartable(void) { 286 ltk_mainloop_init(); 287 while (running) { 288 ltk_mainloop_step(1); 289 } 290 } 291 292 void 293 ltk_mainloop(void) { 294 ltk_mainloop_restartable(); 295 ltk_deinit(); 296 } 297 298 void 299 ltk_deinit(void) { 300 /* if renderdata is NULL, the other initialization can't have happened either */ 301 if (running || !shared_data.renderdata) 302 return; 303 if (shared_data.cmds) { 304 for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) { 305 /* FIXME: maybe kill child processes? */ 306 ltk_free((ltk_array_get(shared_data.cmds, i)).infile); 307 if (ltk_array_get(shared_data.cmds, i).outfile) 308 ltk_free((ltk_array_get(shared_data.cmds, i)).outfile); 309 } 310 ltk_array_destroy(cmdinfo, shared_data.cmds); 311 } 312 shared_data.cmds = NULL; 313 if (shared_data.windows) { 314 for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) { 315 ltk_window *window = ltk_array_get(shared_data.windows, i); 316 ltk_widget_destroy(LTK_CAST_WIDGET(window), 0); 317 } 318 ltk_array_destroy(window, shared_data.windows); 319 } 320 shared_data.windows = NULL; 321 if (shared_data.rwindows) 322 ltk_array_destroy(rwindow, shared_data.rwindows); 323 shared_data.rwindows = NULL; 324 for (size_t i = 0; i < LENGTH(widget_funcs); i++) { 325 if (widget_funcs[i].cleanup) 326 widget_funcs[i].cleanup(); 327 } 328 ltk_config_cleanup(shared_data.renderdata); 329 if (shared_data.text_context) 330 ltk_text_context_destroy(shared_data.text_context); 331 shared_data.text_context = NULL; 332 if (shared_data.clipboard) 333 ltk_clipboard_destroy(shared_data.clipboard); 334 shared_data.clipboard = NULL; 335 ltk_events_cleanup(); 336 ltk_renderer_destroy(shared_data.renderdata); 337 shared_data.renderdata = NULL; 338 } 339 340 /* FIXME: check everywhere if initialized already */ 341 ltk_window * 342 ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h) { 343 /* FIXME: more asserts, or maybe global "initialized" flag */ 344 ltk_assert(shared_data.renderdata != NULL); 345 ltk_assert(shared_data.windows != NULL); 346 ltk_assert(shared_data.rwindows != NULL); 347 ltk_window *window = ltk_window_create_intern(shared_data.renderdata, title, x, y, w, h); 348 ltk_array_append(window, shared_data.windows, window); 349 ltk_array_append(rwindow, shared_data.rwindows, window->renderwindow); 350 return window; 351 } 352 353 void 354 ltk_window_destroy(ltk_widget *self, int shallow) { 355 /* FIXME: would it make sense to do something with 'shallow' here? */ 356 (void)shallow; 357 ltk_window *window = LTK_CAST_WINDOW(self); 358 for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) { 359 if (ltk_array_get(shared_data.windows, i) == window) { 360 ltk_array_delete(window, shared_data.windows, i, 1); 361 ltk_array_delete(rwindow, shared_data.rwindows, i, 1); 362 break; 363 } 364 } 365 ltk_window_destroy_intern(window); 366 } 367 368 ltk_clipboard * 369 ltk_get_clipboard(void) { 370 /* FIXME: what to do when not initialized? */ 371 return shared_data.clipboard; 372 } 373 374 /* FIXME: optimize timer handling - maybe also a sort of priority queue */ 375 /* FIXME: JUST USE A GENERIC DYNAMIC ARRAY ALREADY!!!!! */ 376 void 377 ltk_unregister_timer(int timer_id) { 378 for (size_t i = 0; i < timers_num; i++) { 379 if (timers[i].id == timer_id) { 380 memmove( 381 timers + i, 382 timers + i + 1, 383 sizeof(ltk_timer) * (timers_num - i - 1) 384 ); 385 timers_num--; 386 size_t sz = ideal_array_size(timers_alloc, timers_num); 387 if (sz != timers_alloc) { 388 timers_alloc = sz; 389 timers = ltk_reallocarray( 390 timers, sz, sizeof(ltk_timer) 391 ); 392 } 393 return; 394 } 395 } 396 } 397 398 /* repeat <= 0 means no repeat, first <= 0 means run as soon as possible */ 399 int 400 ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg data), ltk_callback_arg data) { 401 if (first < 0) 402 first = 0; 403 if (repeat < 0) 404 repeat = 0; 405 if (timers_num == timers_alloc) { 406 timers_alloc = ideal_array_size(timers_alloc, timers_num + 1); 407 timers = ltk_reallocarray( 408 timers, timers_alloc, sizeof(ltk_timer) 409 ); 410 } 411 /* FIXME: better finding of id */ 412 /* FIXME: maybe store sorted by id */ 413 int id = 0; 414 for (size_t i = 0; i < timers_num; i++) { 415 if (timers[i].id >= id) 416 id = timers[i].id + 1; 417 } 418 ltk_timer *t = &timers[timers_num++]; 419 t->callback = callback; 420 t->data = data; 421 t->repeat.tv_sec = repeat / 1000; 422 t->repeat.tv_nsec = (repeat % 1000) * 1000; 423 t->remaining.tv_sec = first / 1000; 424 t->remaining.tv_nsec = (first % 1000) * 1000; 425 t->id = id; 426 return id; 427 } 428 429 LTK_ARRAY_INIT_DECL_STATIC(str, char *) 430 LTK_ARRAY_INIT_IMPL_STATIC(str, char *) 431 432 static void 433 str_free_helper(char *elem) { 434 ltk_free(elem); 435 } 436 437 int 438 ltk_call_cmd(ltk_widget *caller, ltk_array(cmd) *cmd, const char *text, size_t textlen) { 439 /* FIXME: maybe support stdin/stdout without temporary files by just piping directly */ 440 /* FIXME: support environment variable $TMPDIR */ 441 ltk_cmdinfo info = { 442 .caller = NULL, .infile = NULL, .outfile = NULL, .pid = -1 443 }; 444 ltk_array(str) *cmdstr = ltk_array_create(str, 4); 445 txtbuf *tmpbuf = txtbuf_new(); 446 int needs_stdin = 1; 447 int needs_stdout = 1; 448 449 int infd = -1, outfd = -1; 450 451 info.infile = ltk_strdup("/tmp/ltk.XXXXXX"); 452 infd = mkstemp(info.infile); 453 if (infd == -1) { 454 ltk_warn_errno("Unable to create temporary input file while trying to run command."); 455 ltk_free(info.infile); 456 info.infile = NULL; /* so it isn't unlinked below */ 457 goto error; 458 } 459 /* FIXME: give file descriptor directly to modified version of ltk_write_file */ 460 char *errstr = NULL; 461 if (ltk_write_file(info.infile, text, textlen, &errstr)) { 462 ltk_warn("Unable to write to temporary input file '%s' while trying to run command.", info.infile, errstr); 463 goto error; 464 } 465 466 for (size_t i = 0; i < ltk_array_len(cmd); i++) { 467 ltk_array(cmdpiece) *pa = ltk_array_get(cmd, i); 468 for (size_t j = 0; j < ltk_array_len(pa); j++) { 469 struct ltk_cmd_piece p = ltk_array_get(pa, j); 470 switch (p.type) { 471 case LTK_CMD_TEXT: 472 txtbuf_append(tmpbuf, p.text); 473 break; 474 case LTK_CMD_INOUT_FILE: 475 needs_stdout = 0; 476 /* fall through */ 477 case LTK_CMD_INPUT_FILE: 478 needs_stdin = 0; 479 txtbuf_append(tmpbuf, info.infile); 480 break; 481 case LTK_CMD_OUTPUT_FILE: 482 needs_stdout = 0; 483 if (!info.outfile) { 484 info.outfile = ltk_strdup("/tmp/ltk.XXXXXX"); 485 outfd = mkstemp(info.outfile); 486 if (outfd == -1) { 487 ltk_warn_errno("Unable to create temporary output file while trying to run command."); 488 ltk_free(info.outfile); 489 info.outfile = NULL; /* so it isn't unlinked below */ 490 goto error; 491 } 492 } 493 txtbuf_append(tmpbuf, info.outfile); 494 break; 495 default: 496 ltk_warn("Invalid command piece type. This should not happen."); 497 goto error; 498 } 499 } 500 ltk_array_append(str, cmdstr, txtbuf_get_textcopy(tmpbuf)); 501 txtbuf_clear(tmpbuf); 502 } 503 /* if no output file was specified, we still need to create it for stdout */ 504 if (needs_stdout) { 505 info.outfile = ltk_strdup("/tmp/ltk.XXXXXX"); 506 outfd = mkstemp(info.outfile); 507 if (outfd == -1) { 508 ltk_warn_errno("Unable to create temporary output file while trying to run command."); 509 ltk_free(info.outfile); 510 info.outfile = NULL; /* so it isn't unlinked below */ 511 goto error; 512 } 513 } 514 ltk_array_append(str, cmdstr, NULL); /* necessary for execve */ 515 txtbuf_destroy(tmpbuf); 516 tmpbuf = NULL; 517 518 int fret = -1; 519 if ((fret = fork()) < 0) { 520 ltk_warn("Unable to fork\n"); 521 goto error; 522 } else if (fret == 0) { 523 if (needs_stdin) { 524 if (dup2(infd, fileno(stdin)) == -1) 525 ltk_fatal("Unable to set up stdin in child process."); 526 } 527 if (needs_stdout) { 528 int fd = outfd == -1 ? infd : outfd; 529 if (dup2(fd, fileno(stdout)) == -1) 530 ltk_fatal("Unable to set up stdout in child process."); 531 } 532 if (execvp(cmdstr->buf[0], cmdstr->buf) == -1) 533 ltk_fatal("Unable to exec external command."); 534 } 535 ltk_array_destroy_deep(str, cmdstr, &str_free_helper); 536 537 info.pid = fret; 538 info.caller = caller; 539 ltk_array_append(cmdinfo, shared_data.cmds, info); 540 541 if (infd != -1) 542 close(infd); /* FIXME: error checking also on close */ 543 if (outfd != -1) 544 close(outfd); 545 return 0; 546 error: 547 if (infd != -1) 548 close(infd); /* FIXME: error checking also on close and unlink */ 549 if (outfd != -1) 550 close(outfd); 551 if (tmpbuf) 552 txtbuf_destroy(tmpbuf); 553 if (info.infile) { 554 unlink(info.infile); 555 ltk_free(info.infile); 556 } 557 if (info.outfile) { 558 unlink(info.outfile); 559 ltk_free(info.outfile); 560 } 561 ltk_array_destroy_deep(str, cmdstr, &str_free_helper); 562 return 1; 563 } 564 565 static void 566 ltk_handle_event(ltk_event *event) { 567 size_t kbd_idx; 568 if (event->type == LTK_KEYBOARDCHANGE_EVENT) { 569 /* FIXME: emit event */ 570 if (ltk_config_get_language_index(event->keyboard.new_kbd, &kbd_idx)) 571 ltk_warn("No language mapping for language \"%s\".\n", event->keyboard.new_kbd); 572 else 573 shared_data.cur_kbd = kbd_idx; 574 } else { 575 if (event->any.window_id < ltk_array_len(shared_data.windows)) { 576 ltk_window_handle_event(ltk_array_get(shared_data.windows, event->any.window_id), event); 577 } 578 } 579 } 580 581 ltk_text_line * 582 ltk_text_line_create_default(const char *font, int font_size, char *text, int take_over_text, int width) { 583 return ltk_text_line_create(shared_data.text_context, font, font_size, text, take_over_text, width); 584 } 585 586 ltk_text_line * 587 ltk_text_line_create_const_text_default(const char *font, int font_size, const char *text, int width) { 588 return ltk_text_line_create_const_text(shared_data.text_context, font, font_size, text, width); 589 }