menu.c (16331B)
1 #define _POSIX_C_SOURCE 200809L 2 #include <assert.h> 3 #include <ctype.h> 4 #include <poll.h> 5 #include <stdbool.h> 6 #include <signal.h> 7 #include <stdio.h> 8 #include <stdlib.h> 9 #include <string.h> 10 #include <strings.h> 11 #include <time.h> 12 #include <unistd.h> 13 #include <sys/mman.h> 14 #include <sys/timerfd.h> 15 #include <wayland-client.h> 16 #include <wayland-client-protocol.h> 17 #include <xkbcommon/xkbcommon.h> 18 19 #include "menu.h" 20 21 #include "pango.h" 22 #include "render.h" 23 #include "wayland.h" 24 25 // Creates and returns a new menu. 26 struct menu *menu_create(menu_callback callback) { 27 struct menu *menu = calloc(1, sizeof(struct menu)); 28 menu->strncmp = strncmp; 29 menu->font = "monospace 12"; 30 menu->normalbg = 0x282828ff; 31 menu->normalfg = 0xfbf1c7ff; 32 menu->promptbg = 0x83a598ff; 33 menu->promptfg = 0xfbf1c7ff; 34 menu->selectionbg = 0x83a598ff; 35 menu->selectionfg = 0xfbf1c7ff; 36 menu->callback = callback; 37 menu->test_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); 38 menu->test_cairo = cairo_create(menu->test_surface); 39 return menu; 40 } 41 42 static void free_pages(struct menu *menu) { 43 struct page *next = menu->pages; 44 while (next) { 45 struct page *page = next; 46 next = page->next; 47 free(page); 48 } 49 } 50 51 static void free_items(struct menu *menu) { 52 for (size_t i = 0; i < menu->item_count; i++) { 53 struct item *item = &menu->items[i]; 54 free(item->text); 55 } 56 free(menu->items); 57 } 58 59 // Destroys the menu, freeing memory associated with it. 60 void menu_destroy(struct menu *menu) { 61 free_pages(menu); 62 free_items(menu); 63 cairo_destroy(menu->test_cairo); 64 cairo_surface_destroy(menu->test_surface); 65 free(menu); 66 } 67 68 static bool parse_color(const char *color, uint32_t *result) { 69 if (color[0] == '#') { 70 ++color; 71 } 72 size_t len = strlen(color); 73 if ((len != 6 && len != 8) || !isxdigit(color[0]) || !isxdigit(color[1])) { 74 return false; 75 } 76 char *ptr; 77 uint32_t parsed = (uint32_t)strtoul(color, &ptr, 16); 78 if (*ptr != '\0') { 79 return false; 80 } 81 *result = len == 6 ? ((parsed << 8) | 0xFF) : parsed; 82 return true; 83 } 84 85 // Parse menu options from command line arguments. 86 void menu_getopts(struct menu *menu, int argc, char *argv[]) { 87 const char *usage = 88 "Usage: wmenu [-biPv] [-f font] [-l lines] [-o output] [-p prompt]\n" 89 "\t[-N color] [-n color] [-M color] [-m color] [-S color] [-s color]\n"; 90 91 int opt; 92 while ((opt = getopt(argc, argv, "bhiPvf:l:o:p:N:n:M:m:S:s:")) != -1) { 93 switch (opt) { 94 case 'b': 95 menu->bottom = true; 96 break; 97 case 'i': 98 menu->strncmp = strncasecmp; 99 break; 100 case 'P': 101 menu->passwd = true; 102 break; 103 case 'v': 104 puts("wmenu " VERSION); 105 exit(EXIT_SUCCESS); 106 case 'f': 107 menu->font = optarg; 108 break; 109 case 'l': 110 menu->lines = atoi(optarg); 111 break; 112 case 'o': 113 menu->output_name = optarg; 114 break; 115 case 'p': 116 menu->prompt = optarg; 117 break; 118 case 'N': 119 if (!parse_color(optarg, &menu->normalbg)) { 120 fprintf(stderr, "Invalid background color: %s", optarg); 121 } 122 break; 123 case 'n': 124 if (!parse_color(optarg, &menu->normalfg)) { 125 fprintf(stderr, "Invalid foreground color: %s", optarg); 126 } 127 break; 128 case 'M': 129 if (!parse_color(optarg, &menu->promptbg)) { 130 fprintf(stderr, "Invalid prompt background color: %s", optarg); 131 } 132 break; 133 case 'm': 134 if (!parse_color(optarg, &menu->promptfg)) { 135 fprintf(stderr, "Invalid prompt foreground color: %s", optarg); 136 } 137 break; 138 case 'S': 139 if (!parse_color(optarg, &menu->selectionbg)) { 140 fprintf(stderr, "Invalid selection background color: %s", optarg); 141 } 142 break; 143 case 's': 144 if (!parse_color(optarg, &menu->selectionfg)) { 145 fprintf(stderr, "Invalid selection foreground color: %s", optarg); 146 } 147 break; 148 default: 149 fprintf(stderr, "%s", usage); 150 exit(EXIT_FAILURE); 151 } 152 } 153 154 if (optind < argc) { 155 fprintf(stderr, "%s", usage); 156 exit(EXIT_FAILURE); 157 } 158 159 int height = get_font_height(menu->font); 160 menu->line_height = height + 2; 161 menu->height = menu->line_height; 162 if (menu->lines > 0) { 163 menu->height += menu->height * menu->lines; 164 } 165 menu->padding = height / 2; 166 } 167 168 // Add an item to the menu. 169 void menu_add_item(struct menu *menu, char *text) { 170 if ((menu->item_count & (menu->item_count - 1)) == 0) { 171 size_t alloc_size = menu->item_count ? 2 * menu->item_count : 1; 172 void *new_array = realloc(menu->items, sizeof(struct item) * alloc_size); 173 if (!new_array) { 174 fprintf(stderr, "could not realloc %zu bytes", sizeof(struct item) * alloc_size); 175 exit(EXIT_FAILURE); 176 } 177 menu->items = new_array; 178 } 179 180 struct item *new = &menu->items[menu->item_count]; 181 new->text = text; 182 183 menu->item_count++; 184 } 185 186 static int compare_items(const void *a, const void *b) { 187 const struct item *item_a = a; 188 const struct item *item_b = b; 189 return strcmp(item_a->text, item_b->text); 190 } 191 192 void menu_sort_and_deduplicate(struct menu *menu) { 193 size_t j = 1; 194 size_t i; 195 196 qsort(menu->items, menu->item_count, sizeof(*menu->items), compare_items); 197 198 for (i = 1; i < menu->item_count; i++) { 199 if (strcmp(menu->items[i].text, menu->items[j - 1].text) == 0) { 200 free(menu->items[i].text); 201 } else { 202 menu->items[j] = menu->items[i]; 203 j++; 204 } 205 } 206 menu->item_count = j; 207 } 208 209 static void append_page(struct page *page, struct page **first, struct page **last) { 210 if (*last) { 211 (*last)->next = page; 212 } else { 213 *first = page; 214 } 215 page->prev = *last; 216 page->next = NULL; 217 *last = page; 218 } 219 220 static void page_items(struct menu *menu) { 221 // Free existing pages 222 while (menu->pages != NULL) { 223 struct page *page = menu->pages; 224 menu->pages = menu->pages->next; 225 free(page); 226 } 227 228 if (!menu->matches) { 229 return; 230 } 231 232 // Make new pages 233 if (menu->lines > 0) { 234 struct page *pages_end = NULL; 235 struct item *item = menu->matches; 236 while (item) { 237 struct page *page = calloc(1, sizeof(struct page)); 238 page->first = item; 239 240 for (int i = 1; item && i <= menu->lines; i++) { 241 item->page = page; 242 page->last = item; 243 item = item->next_match; 244 } 245 append_page(page, &menu->pages, &pages_end); 246 } 247 } else { 248 // Calculate available space 249 int max_width = menu->width - menu->inputw - menu->promptw 250 - menu->left_arrow - menu->right_arrow; 251 252 struct page *pages_end = NULL; 253 struct item *item = menu->matches; 254 while (item) { 255 struct page *page = calloc(1, sizeof(struct page)); 256 page->first = item; 257 258 int total_width = 0; 259 int items = 0; 260 while (item) { 261 total_width += item->width + 2 * menu->padding; 262 if (total_width > max_width && items > 0) { 263 break; 264 } 265 items++; 266 267 item->page = page; 268 page->last = item; 269 item = item->next_match; 270 } 271 append_page(page, &menu->pages, &pages_end); 272 } 273 } 274 } 275 276 static const char *fstrstr(struct menu *menu, const char *s, const char *sub) { 277 for (size_t len = strlen(sub); *s; s++) { 278 if (!menu->strncmp(s, sub, len)) { 279 return s; 280 } 281 } 282 return NULL; 283 } 284 285 static void append_match(struct item *item, struct item **first, struct item **last) { 286 if (*last) { 287 (*last)->next_match = item; 288 } else { 289 *first = item; 290 } 291 item->prev_match = *last; 292 item->next_match = NULL; 293 *last = item; 294 } 295 296 static void match_items(struct menu *menu) { 297 struct item *lexact = NULL, *exactend = NULL; 298 struct item *lprefix = NULL, *prefixend = NULL; 299 struct item *lsubstr = NULL, *substrend = NULL; 300 char buf[sizeof menu->input], *tok; 301 char **tokv = NULL; 302 int i, tokc = 0; 303 size_t k; 304 size_t tok_len; 305 menu->matches = NULL; 306 menu->matches_end = NULL; 307 menu->sel = NULL; 308 309 size_t input_len = strlen(menu->input); 310 311 /* tokenize input by space for matching the tokens individually */ 312 strcpy(buf, menu->input); 313 tok = strtok(buf, " "); 314 while (tok) { 315 tokv = realloc(tokv, (tokc + 1) * sizeof *tokv); 316 if (!tokv) { 317 fprintf(stderr, "could not realloc %zu bytes", 318 (tokc + 1) * sizeof *tokv); 319 exit(EXIT_FAILURE); 320 } 321 tokv[tokc] = tok; 322 tokc++; 323 tok = strtok(NULL, " "); 324 } 325 tok_len = tokc ? strlen(tokv[0]) : 0; 326 327 for (k = 0; k < menu->item_count; k++) { 328 struct item *item = &menu->items[k]; 329 for (i = 0; i < tokc; i++) { 330 if (!fstrstr(menu, item->text, tokv[i])) { 331 /* token does not match */ 332 break; 333 } 334 } 335 if (i != tokc) { 336 /* not all tokens match */ 337 continue; 338 } 339 if (!tokc || !menu->strncmp(menu->input, item->text, input_len + 1)) { 340 append_match(item, &lexact, &exactend); 341 } else if (!menu->strncmp(tokv[0], item->text, tok_len)) { 342 append_match(item, &lprefix, &prefixend); 343 } else { 344 append_match(item, &lsubstr, &substrend); 345 } 346 } 347 348 free(tokv); 349 350 if (lexact) { 351 menu->matches = lexact; 352 menu->matches_end = exactend; 353 } 354 if (lprefix) { 355 if (menu->matches_end) { 356 menu->matches_end->next_match = lprefix; 357 lprefix->prev_match = menu->matches_end; 358 } else { 359 menu->matches = lprefix; 360 } 361 menu->matches_end = prefixend; 362 } 363 if (lsubstr) { 364 if (menu->matches_end) { 365 menu->matches_end->next_match = lsubstr; 366 lsubstr->prev_match = menu->matches_end; 367 } else { 368 menu->matches = lsubstr; 369 } 370 menu->matches_end = substrend; 371 } 372 373 page_items(menu); 374 if (menu->pages) { 375 menu->sel = menu->pages->first; 376 } 377 } 378 379 // Marks the menu as needing to be rendered again. 380 void menu_invalidate(struct menu *menu) { 381 menu->rendered = false; 382 } 383 384 // Render menu items. 385 void menu_render_items(struct menu *menu) { 386 calc_widths(menu); 387 match_items(menu); 388 render_menu(menu); 389 } 390 391 static void insert(struct menu *menu, const char *text, ssize_t len) { 392 if (strlen(menu->input) + len > sizeof menu->input - 1) { 393 return; 394 } 395 memmove(menu->input + menu->cursor + len, menu->input + menu->cursor, 396 sizeof menu->input - menu->cursor - MAX(len, 0)); 397 if (len > 0 && text != NULL) { 398 memcpy(menu->input + menu->cursor, text, len); 399 } 400 menu->cursor += len; 401 } 402 403 // Add pasted text to the menu input. 404 void menu_paste(struct menu *menu, const char *text, ssize_t len) { 405 insert(menu, text, len); 406 } 407 408 static size_t nextrune(struct menu *menu, int incr) { 409 size_t n, len; 410 411 len = strlen(menu->input); 412 for(n = menu->cursor + incr; n < len && (menu->input[n] & 0xc0) == 0x80; n += incr); 413 return n; 414 } 415 416 // Move the cursor to the beginning or end of the word, skipping over any preceding whitespace. 417 static void movewordedge(struct menu *menu, int dir) { 418 if (dir < 0) { 419 // Move to beginning of word 420 while (menu->cursor > 0 && menu->input[nextrune(menu, -1)] == ' ') { 421 menu->cursor = nextrune(menu, -1); 422 } 423 while (menu->cursor > 0 && menu->input[nextrune(menu, -1)] != ' ') { 424 menu->cursor = nextrune(menu, -1); 425 } 426 } else { 427 // Move to end of word 428 size_t len = strlen(menu->input); 429 while (menu->cursor < len && menu->input[menu->cursor] == ' ') { 430 menu->cursor = nextrune(menu, +1); 431 } 432 while (menu->cursor < len && menu->input[menu->cursor] != ' ') { 433 menu->cursor = nextrune(menu, +1); 434 } 435 } 436 } 437 438 // Handle a keypress. 439 void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, 440 xkb_keysym_t sym) { 441 if (key_state != WL_KEYBOARD_KEY_STATE_PRESSED) { 442 return; 443 } 444 445 struct xkb_state *state = context_get_xkb_state(menu->context); 446 bool ctrl = xkb_state_mod_name_is_active(state, XKB_MOD_NAME_CTRL, 447 XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); 448 bool meta = xkb_state_mod_name_is_active(state, XKB_MOD_NAME_ALT, 449 XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); 450 bool shift = xkb_state_mod_name_is_active(state, XKB_MOD_NAME_SHIFT, 451 XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); 452 453 size_t len = strlen(menu->input); 454 455 if (ctrl) { 456 // Emacs-style line editing bindings 457 switch (sym) { 458 case XKB_KEY_a: 459 sym = XKB_KEY_Home; 460 break; 461 case XKB_KEY_b: 462 sym = XKB_KEY_Left; 463 break; 464 case XKB_KEY_c: 465 sym = XKB_KEY_Escape; 466 break; 467 case XKB_KEY_d: 468 sym = XKB_KEY_Delete; 469 break; 470 case XKB_KEY_e: 471 sym = XKB_KEY_End; 472 break; 473 case XKB_KEY_f: 474 sym = XKB_KEY_Right; 475 break; 476 case XKB_KEY_g: 477 sym = XKB_KEY_Escape; 478 break; 479 case XKB_KEY_bracketleft: 480 sym = XKB_KEY_Escape; 481 break; 482 case XKB_KEY_h: 483 sym = XKB_KEY_BackSpace; 484 break; 485 case XKB_KEY_i: 486 sym = XKB_KEY_Tab; 487 break; 488 case XKB_KEY_j: 489 case XKB_KEY_J: 490 case XKB_KEY_m: 491 case XKB_KEY_M: 492 sym = XKB_KEY_Return; 493 ctrl = false; 494 break; 495 case XKB_KEY_n: 496 sym = XKB_KEY_Down; 497 break; 498 case XKB_KEY_p: 499 sym = XKB_KEY_Up; 500 break; 501 502 case XKB_KEY_k: 503 // Delete right 504 menu->input[menu->cursor] = '\0'; 505 match_items(menu); 506 menu_invalidate(menu); 507 return; 508 case XKB_KEY_u: 509 // Delete left 510 insert(menu, NULL, 0 - menu->cursor); 511 match_items(menu); 512 menu_invalidate(menu); 513 return; 514 case XKB_KEY_w: 515 // Delete word 516 while (menu->cursor > 0 && menu->input[nextrune(menu, -1)] == ' ') { 517 insert(menu, NULL, nextrune(menu, -1) - menu->cursor); 518 } 519 while (menu->cursor > 0 && menu->input[nextrune(menu, -1)] != ' ') { 520 insert(menu, NULL, nextrune(menu, -1) - menu->cursor); 521 } 522 match_items(menu); 523 menu_invalidate(menu); 524 return; 525 case XKB_KEY_Y: 526 // Paste clipboard 527 if (!context_paste(menu->context)) { 528 return; 529 } 530 match_items(menu); 531 menu_invalidate(menu); 532 return; 533 case XKB_KEY_Left: 534 case XKB_KEY_KP_Left: 535 movewordedge(menu, -1); 536 menu_invalidate(menu); 537 return; 538 case XKB_KEY_Right: 539 case XKB_KEY_KP_Right: 540 movewordedge(menu, +1); 541 menu_invalidate(menu); 542 return; 543 544 case XKB_KEY_Return: 545 case XKB_KEY_KP_Enter: 546 break; 547 default: 548 return; 549 } 550 } else if (meta) { 551 // Emacs-style line editing bindings 552 switch (sym) { 553 case XKB_KEY_b: 554 movewordedge(menu, -1); 555 menu_invalidate(menu); 556 return; 557 case XKB_KEY_f: 558 movewordedge(menu, +1); 559 menu_invalidate(menu); 560 return; 561 case XKB_KEY_g: 562 sym = XKB_KEY_Home; 563 break; 564 case XKB_KEY_G: 565 sym = XKB_KEY_End; 566 break; 567 case XKB_KEY_h: 568 sym = XKB_KEY_Up; 569 break; 570 case XKB_KEY_j: 571 sym = XKB_KEY_Next; 572 break; 573 case XKB_KEY_k: 574 sym = XKB_KEY_Prior; 575 break; 576 case XKB_KEY_l: 577 sym = XKB_KEY_Down; 578 break; 579 default: 580 return; 581 } 582 } 583 584 char buf[8]; 585 switch (sym) { 586 case XKB_KEY_Return: 587 case XKB_KEY_KP_Enter: 588 if (shift) { 589 menu->callback(menu, menu->input, true); 590 } else { 591 char *text = menu->sel ? menu->sel->text : menu->input; 592 menu->callback(menu, text, !ctrl); 593 } 594 break; 595 case XKB_KEY_Left: 596 case XKB_KEY_KP_Left: 597 case XKB_KEY_Up: 598 case XKB_KEY_KP_Up: 599 if (menu->sel && menu->sel->prev_match) { 600 menu->sel = menu->sel->prev_match; 601 menu_invalidate(menu); 602 } else if (menu->cursor > 0) { 603 menu->cursor = nextrune(menu, -1); 604 menu_invalidate(menu); 605 } 606 break; 607 case XKB_KEY_Right: 608 case XKB_KEY_KP_Right: 609 case XKB_KEY_Down: 610 case XKB_KEY_KP_Down: 611 if (menu->cursor < len) { 612 menu->cursor = nextrune(menu, +1); 613 menu_invalidate(menu); 614 } else if (menu->sel && menu->sel->next_match) { 615 menu->sel = menu->sel->next_match; 616 menu_invalidate(menu); 617 } 618 break; 619 case XKB_KEY_Prior: 620 case XKB_KEY_KP_Prior: 621 if (menu->sel && menu->sel->page->prev) { 622 menu->sel = menu->sel->page->prev->first; 623 menu_invalidate(menu); 624 } 625 break; 626 case XKB_KEY_Next: 627 case XKB_KEY_KP_Next: 628 if (menu->sel && menu->sel->page->next) { 629 menu->sel = menu->sel->page->next->first; 630 menu_invalidate(menu); 631 } 632 break; 633 case XKB_KEY_Home: 634 case XKB_KEY_KP_Home: 635 if (menu->sel == menu->matches) { 636 menu->cursor = 0; 637 menu_invalidate(menu); 638 } else { 639 menu->sel = menu->matches; 640 menu_invalidate(menu); 641 } 642 break; 643 case XKB_KEY_End: 644 case XKB_KEY_KP_End: 645 if (menu->cursor < len) { 646 menu->cursor = len; 647 menu_invalidate(menu); 648 } else { 649 menu->sel = menu->matches_end; 650 menu_invalidate(menu); 651 } 652 break; 653 case XKB_KEY_BackSpace: 654 if (menu->cursor > 0) { 655 insert(menu, NULL, nextrune(menu, -1) - menu->cursor); 656 match_items(menu); 657 menu_invalidate(menu); 658 } 659 break; 660 case XKB_KEY_Delete: 661 case XKB_KEY_KP_Delete: 662 if (menu->cursor == len) { 663 return; 664 } 665 menu->cursor = nextrune(menu, +1); 666 insert(menu, NULL, nextrune(menu, -1) - menu->cursor); 667 match_items(menu); 668 menu_invalidate(menu); 669 break; 670 case XKB_KEY_Tab: 671 if (!menu->sel) { 672 return; 673 } 674 menu->cursor = strnlen(menu->sel->text, sizeof menu->input - 1); 675 memcpy(menu->input, menu->sel->text, menu->cursor); 676 menu->input[menu->cursor] = '\0'; 677 match_items(menu); 678 menu_invalidate(menu); 679 break; 680 case XKB_KEY_Escape: 681 menu->exit = true; 682 menu->failure = true; 683 break; 684 default: 685 if (xkb_keysym_to_utf8(sym, buf, 8)) { 686 insert(menu, buf, strnlen(buf, 8)); 687 match_items(menu); 688 menu_invalidate(menu); 689 } 690 } 691 }