dmenu.c (17032B)
1 /* See LICENSE file for copyright and license details. */ 2 #include <ctype.h> 3 #include <errno.h> 4 #include <fcntl.h> 5 #include <locale.h> 6 #include <poll.h> 7 #include <signal.h> 8 #include <stdio.h> 9 #include <stdlib.h> 10 #include <string.h> 11 #include <strings.h> 12 #include <sys/ioctl.h> 13 #include <term.h> 14 #include <termios.h> 15 #include <unistd.h> 16 17 #include "drw.h" 18 #include "util.h" 19 20 #ifdef lines 21 #undef lines 22 #endif 23 24 /* macros */ 25 #define ITEMGAP 1 26 #define TEXTW(X) (drw_fontset_getwidth(drw, (X)) + lrpad) 27 28 /* enums */ 29 enum { SchemeNorm, SchemeSel, SchemeOut, SchemeLast }; /* color schemes */ 30 31 struct item { 32 char *text; 33 struct item *left, *right; 34 int out; 35 }; 36 37 static char text[BUFSIZ] = ""; 38 static int bh, mw, mh; 39 static int inputw = 0, promptw; 40 static int lrpad; /* sum of left and right padding */ 41 static int termh, yoff; 42 static int passwd; 43 static size_t cursor; 44 static struct item *items = NULL; 45 static struct item *matches, *matchend; 46 static struct item *prev, *curr, *next, *sel; 47 static volatile sig_atomic_t resized; 48 static int ttyfd = -1; 49 static FILE *ttyout; 50 static struct termios termios_orig; 51 static int termios_saved; 52 53 static Drw *drw; 54 static Clr *scheme[SchemeLast]; 55 56 #include "config.h" 57 58 static int (*fstrncmp)(const char *, const char *, size_t) = strncmp; 59 static char *(*fstrstr)(const char *, const char *) = strstr; 60 61 enum { 62 XK_Left = 0x101, 63 XK_Right, 64 XK_Up, 65 XK_Down, 66 XK_Home, 67 XK_End, 68 XK_Next, 69 XK_Prior, 70 XK_Delete, 71 XK_BackSpace, 72 XK_Return, 73 XK_KP_Left, 74 XK_KP_Right, 75 XK_KP_Up, 76 XK_KP_Down, 77 XK_KP_Home, 78 XK_KP_End, 79 XK_KP_Next, 80 XK_KP_Prior, 81 XK_KP_Delete, 82 XK_KP_Enter, 83 XK_Escape = 27, 84 XK_Tab = '\t' 85 }; 86 87 static unsigned int 88 itemw_clamp(const char *str, unsigned int n) 89 { 90 unsigned int w = drw_fontset_getwidth_clamp(drw, str, n > ITEMGAP ? n - ITEMGAP : 0) + lrpad + ITEMGAP; 91 return MIN(w, n); 92 } 93 94 static const char * 95 inputtext(void) 96 { 97 return passwd ? "" : text; 98 } 99 100 static void 101 appenditem(struct item *item, struct item **list, struct item **last) 102 { 103 if (*last) 104 (*last)->right = item; 105 else 106 *list = item; 107 108 item->left = *last; 109 item->right = NULL; 110 *last = item; 111 } 112 113 static void 114 calcoffsets(void) 115 { 116 int i, n; 117 118 if (lines > 0) 119 n = lines * bh; 120 else 121 n = mw - (promptw + inputw + TEXTW("<") + TEXTW(">")); 122 /* calculate which items will begin the next page and previous page */ 123 for (i = 0, next = curr; next; next = next->right) 124 if ((i += (lines > 0) ? bh : itemw_clamp(next->text, n)) > n) 125 break; 126 for (i = 0, prev = curr; prev && prev->left; prev = prev->left) 127 if ((i += (lines > 0) ? bh : itemw_clamp(prev->left->text, n)) > n) 128 break; 129 } 130 131 static void 132 cleanup(void) 133 { 134 size_t i; 135 136 for (i = 0; i < SchemeLast; i++) 137 drw_scm_free(drw, scheme[i], 2); 138 for (i = 0; items && items[i].text; ++i) 139 free(items[i].text); 140 free(items); 141 drw_cursor_restore(); 142 drw_term_reset(); 143 drw_free(drw); 144 if (termios_saved) 145 tcsetattr(ttyfd, TCSAFLUSH, &termios_orig); 146 if (ttyout) 147 fclose(ttyout); 148 if (ttyfd >= 0) 149 close(ttyfd); 150 } 151 152 static char * 153 cistrstr(const char *h, const char *n) 154 { 155 size_t i; 156 157 if (!n[0]) 158 return (char *)h; 159 160 for (; *h; ++h) { 161 for (i = 0; n[i] && tolower((unsigned char)n[i]) == 162 tolower((unsigned char)h[i]); ++i) 163 ; 164 if (n[i] == '\0') 165 return (char *)h; 166 } 167 return NULL; 168 } 169 170 static int 171 drawitem(struct item *item, int x, int y, int w) 172 { 173 int lpad = lrpad / 2; 174 int draww = w; 175 176 if (item == sel) 177 drw_setscheme(drw, scheme[SchemeSel]); 178 else if (item->out) 179 drw_setscheme(drw, scheme[SchemeOut]); 180 else 181 drw_setscheme(drw, scheme[SchemeNorm]); 182 183 if (item == sel) { 184 /* Keep selection background tight: one cell before and after text. */ 185 lpad = 1; 186 draww = MIN(w, (int)drw_fontset_getwidth(drw, item->text) + 2); 187 } 188 return drw_text(drw, x, y, draww, bh, lpad, item->text, 0) + ITEMGAP; 189 } 190 191 static void 192 drawmenu(void) 193 { 194 unsigned int curpos; 195 const char *displaytext; 196 struct item *item; 197 int cursorx, x = 0, y = yoff, w; 198 199 drw_setscheme(drw, scheme[SchemeNorm]); 200 drw_rect(drw, 0, yoff, mw, mh, 1, 0); 201 202 if (prompt && *prompt) { 203 drw_setscheme(drw, scheme[SchemeSel]); 204 x = drw_text(drw, x, yoff, promptw, bh, lrpad / 2, prompt, 0); 205 } 206 /* draw input field */ 207 w = (lines > 0 || !matches) ? mw - x : inputw; 208 drw_setscheme(drw, scheme[SchemeNorm]); 209 displaytext = inputtext(); 210 drw_text(drw, x, yoff, w, bh, lrpad / 2, displaytext, 0); 211 212 curpos = TEXTW(displaytext) - TEXTW(&displaytext[passwd ? 0 : cursor]); 213 cursorx = x; 214 curpos += lrpad / 2; 215 216 if (lines > 0) { 217 /* draw vertical list */ 218 for (item = curr; item != next; item = item->right) 219 drawitem(item, x, y += bh, mw - x); 220 } else if (matches) { 221 /* draw horizontal list */ 222 x += inputw; 223 w = TEXTW("<"); 224 if (curr->left) { 225 drw_setscheme(drw, scheme[SchemeNorm]); 226 drw_text(drw, x, yoff, w, bh, lrpad / 2, "<", 0); 227 } 228 x += w; 229 for (item = curr; item != next; item = item->right) 230 x = drawitem(item, x, yoff, itemw_clamp(item->text, mw - x - TEXTW(">"))); 231 if (next) { 232 w = TEXTW(">"); 233 drw_setscheme(drw, scheme[SchemeNorm]); 234 drw_text(drw, mw - w, yoff, w, bh, lrpad / 2, ">", 0); 235 } 236 } 237 drw_move(drw, MIN(cursorx + (int)curpos, mw - 1), yoff); 238 drw_map(drw, 0, 0, 0, mw, mh); 239 } 240 241 static void 242 match(void) 243 { 244 static char **tokv = NULL; 245 static int tokn = 0; 246 char buf[sizeof text], *s; 247 int i, tokc = 0; 248 size_t len, textsize; 249 struct item *item, *lprefix, *lsubstr, *prefixend, *substrend; 250 251 strcpy(buf, text); 252 /* separate input text into tokens to be matched individually */ 253 for (s = strtok(buf, " "); s; tokv[tokc - 1] = s, s = strtok(NULL, " ")) 254 if (++tokc > tokn && !(tokv = realloc(tokv, ++tokn * sizeof *tokv))) 255 die("cannot realloc %zu bytes:", tokn * sizeof *tokv); 256 len = tokc ? strlen(tokv[0]) : 0; 257 258 matches = lprefix = lsubstr = matchend = prefixend = substrend = NULL; 259 textsize = strlen(text) + 1; 260 for (item = items; item && item->text; item++) { 261 for (i = 0; i < tokc; i++) 262 if (!fstrstr(item->text, tokv[i])) 263 break; 264 if (i != tokc) /* not all tokens match */ 265 continue; 266 /* exact matches go first, then prefixes, then substrings */ 267 if (!tokc || !fstrncmp(text, item->text, textsize)) 268 appenditem(item, &matches, &matchend); 269 else if (!fstrncmp(tokv[0], item->text, len)) 270 appenditem(item, &lprefix, &prefixend); 271 else 272 appenditem(item, &lsubstr, &substrend); 273 } 274 if (lprefix) { 275 if (matches) { 276 matchend->right = lprefix; 277 lprefix->left = matchend; 278 } else 279 matches = lprefix; 280 matchend = prefixend; 281 } 282 if (lsubstr) { 283 if (matches) { 284 matchend->right = lsubstr; 285 lsubstr->left = matchend; 286 } else 287 matches = lsubstr; 288 matchend = substrend; 289 } 290 curr = sel = matches; 291 calcoffsets(); 292 } 293 294 static void 295 insert(const char *str, ssize_t n) 296 { 297 if (strlen(text) + n > sizeof text - 1) 298 return; 299 /* move existing text out of the way, insert new text, and update cursor */ 300 memmove(&text[cursor + n], &text[cursor], sizeof text - cursor - MAX(n, 0)); 301 if (n > 0) 302 memcpy(&text[cursor], str, n); 303 cursor += n; 304 match(); 305 } 306 307 static size_t 308 nextrune(int inc) 309 { 310 ssize_t n; 311 312 /* return location of next utf8 rune in the given direction (+1 or -1) */ 313 for (n = cursor + inc; n + inc >= 0 && (text[n] & 0xc0) == 0x80; n += inc) 314 ; 315 return n; 316 } 317 318 static void 319 movewordedge(int dir) 320 { 321 if (dir < 0) { /* move cursor to the start of the word*/ 322 while (cursor > 0 && strchr(worddelimiters, text[nextrune(-1)])) 323 cursor = nextrune(-1); 324 while (cursor > 0 && !strchr(worddelimiters, text[nextrune(-1)])) 325 cursor = nextrune(-1); 326 } else { /* move cursor to the end of the word */ 327 while (text[cursor] && strchr(worddelimiters, text[cursor])) 328 cursor = nextrune(+1); 329 while (text[cursor] && !strchr(worddelimiters, text[cursor])) 330 cursor = nextrune(+1); 331 } 332 } 333 334 static void 335 resize(void) 336 { 337 struct winsize ws; 338 339 resized = 0; 340 if (ioctl(ttyfd, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0 || ws.ws_row == 0) { 341 mw = 80; 342 termh = 24; 343 } else { 344 mw = ws.ws_col; 345 termh = ws.ws_row; 346 } 347 bh = drw->fonts->h; 348 mh = lines + 1; 349 yoff = topbar ? 0 : MAX(termh - mh, 0); 350 promptw = (prompt && *prompt) ? TEXTW(prompt) - lrpad / 4 : 0; 351 inputw = mw / 3; 352 drw_resize(drw, mw, mh); 353 calcoffsets(); 354 drawmenu(); 355 } 356 357 static void 358 sigwinch(int unused) 359 { 360 (void)unused; 361 resized = 1; 362 } 363 364 static int 365 readkey(void) 366 { 367 unsigned char ch = 0, seq[3]; 368 struct pollfd pfd = { .fd = ttyfd, .events = POLLIN }; 369 int n; 370 371 n = read(ttyfd, &ch, 1); 372 if (n <= 0) { 373 if (errno == EINTR && resized) 374 return 0; 375 return 0; 376 } 377 if (ch == '\r') 378 return '\n'; 379 if (ch == 127 || ch == 8) 380 return XK_BackSpace; 381 if (ch != 27) 382 return ch; 383 384 if (poll(&pfd, 1, 25) <= 0) 385 return 27; 386 if (read(ttyfd, &seq[0], 1) != 1) 387 return 27; 388 389 if (seq[0] == '[') { 390 if (read(ttyfd, &seq[1], 1) != 1) 391 return 27; 392 if (seq[1] >= '0' && seq[1] <= '9') { 393 if (read(ttyfd, &seq[2], 1) != 1) 394 return 27; 395 if (seq[2] != '~') 396 return 27; 397 switch (seq[1]) { 398 case '1': return XK_Home; 399 case '3': return XK_Delete; 400 case '4': return XK_End; 401 case '5': return XK_Prior; 402 case '6': return XK_Next; 403 case '7': return XK_Home; 404 case '8': return XK_End; 405 default: return 27; 406 } 407 } 408 switch (seq[1]) { 409 case 'A': return XK_Up; 410 case 'B': return XK_Down; 411 case 'C': return XK_Right; 412 case 'D': return XK_Left; 413 case 'H': return XK_Home; 414 case 'F': return XK_End; 415 default: return 27; 416 } 417 } 418 if (seq[0] == 'O') { 419 if (read(ttyfd, &seq[1], 1) != 1) 420 return 27; 421 switch (seq[1]) { 422 case 'H': return XK_Home; 423 case 'F': return XK_End; 424 default: return 27; 425 } 426 } 427 return 0x200 | seq[0]; 428 } 429 430 static void 431 keypress(int ch) 432 { 433 char buf[7]; 434 int len = 0, alt = ch & 0x200; 435 436 ch &= ~0x200; 437 switch (alt ? ch : 0) { 438 case 'b': 439 movewordedge(-1); 440 goto draw; 441 case 'f': 442 movewordedge(+1); 443 goto draw; 444 case 'g': 445 ch = XK_Home; 446 break; 447 case 'G': 448 ch = XK_End; 449 break; 450 case 'h': 451 ch = XK_Up; 452 break; 453 case 'j': 454 ch = XK_Next; 455 break; 456 case 'k': 457 ch = XK_Prior; 458 break; 459 case 'l': 460 ch = XK_Down; 461 break; 462 default: 463 break; 464 } 465 466 if (!alt && ch >= 1 && ch <= 26) { 467 switch (ch) { 468 case 1: ch = XK_Home; break; 469 case 2: ch = XK_Left; break; 470 case 3: 471 case 7: 472 cleanup(); 473 exit(1); 474 case 4: ch = XK_Delete; break; 475 case 5: ch = XK_End; break; 476 case 6: ch = XK_Right; break; 477 case 8: ch = XK_BackSpace; break; 478 case 9: ch = '\t'; break; 479 case 10: 480 case 13: ch = '\n'; break; 481 case 11: 482 text[cursor] = '\0'; 483 match(); 484 goto draw; 485 case 14: ch = XK_Down; break; 486 case 16: ch = XK_Up; break; 487 case 21: 488 insert(NULL, 0 - cursor); 489 goto draw; 490 case 23: 491 while (cursor > 0 && strchr(worddelimiters, text[nextrune(-1)])) 492 insert(NULL, nextrune(-1) - cursor); 493 while (cursor > 0 && !strchr(worddelimiters, text[nextrune(-1)])) 494 insert(NULL, nextrune(-1) - cursor); 495 goto draw; 496 case 25: 497 goto draw; 498 default: 499 return; 500 } 501 } 502 503 switch (ch) { 504 default: 505 if (!iscntrl((unsigned char)ch)) { 506 buf[0] = ch; 507 len = 1; 508 insert(buf, len); 509 } 510 break; 511 case XK_Delete: 512 case XK_KP_Delete: 513 if (text[cursor] == '\0') 514 return; 515 cursor = nextrune(+1); 516 /* fallthrough */ 517 case XK_BackSpace: 518 case 127: 519 if (cursor == 0) 520 return; 521 insert(NULL, nextrune(-1) - cursor); 522 break; 523 case XK_End: 524 case XK_KP_End: 525 if (text[cursor] != '\0') { 526 cursor = strlen(text); 527 break; 528 } 529 if (next) { 530 /* jump to end of list and position items in reverse */ 531 curr = matchend; 532 calcoffsets(); 533 curr = prev; 534 calcoffsets(); 535 while (next && (curr = curr->right)) 536 calcoffsets(); 537 } 538 sel = matchend; 539 break; 540 case XK_Escape: 541 cleanup(); 542 exit(1); 543 case XK_Home: 544 case XK_KP_Home: 545 if (sel == matches) { 546 cursor = 0; 547 break; 548 } 549 sel = curr = matches; 550 calcoffsets(); 551 break; 552 case XK_Left: 553 case XK_KP_Left: 554 if (cursor > 0 && (!sel || !sel->left || lines > 0)) { 555 cursor = nextrune(-1); 556 break; 557 } 558 if (lines > 0) 559 return; 560 /* fallthrough */ 561 case XK_Up: 562 case XK_KP_Up: 563 if (sel && sel->left && (sel = sel->left)->right == curr) { 564 curr = prev; 565 calcoffsets(); 566 } 567 break; 568 case XK_Next: 569 case XK_KP_Next: 570 if (!next) 571 return; 572 sel = curr = next; 573 calcoffsets(); 574 break; 575 case XK_Prior: 576 case XK_KP_Prior: 577 if (!prev) 578 return; 579 sel = curr = prev; 580 calcoffsets(); 581 break; 582 case '\n': 583 case XK_Return: 584 case XK_KP_Enter: 585 if (sel && sel->text) 586 buf[0] = '\0'; 587 if (sel && sel->text) { 588 char *s; 589 590 if (!(s = strdup(sel->text))) 591 die("strdup:"); 592 cleanup(); 593 puts(s); 594 free(s); 595 exit(0); 596 } 597 cleanup(); 598 puts(text); 599 exit(0); 600 case XK_Right: 601 case XK_KP_Right: 602 if (text[cursor] != '\0') { 603 cursor = nextrune(+1); 604 break; 605 } 606 if (lines > 0) 607 return; 608 /* fallthrough */ 609 case XK_Down: 610 case XK_KP_Down: 611 if (sel && sel->right && (sel = sel->right) == next) { 612 curr = next; 613 calcoffsets(); 614 } 615 break; 616 case XK_Tab: 617 if (!sel) 618 return; 619 cursor = strnlen(sel->text, sizeof text - 1); 620 memcpy(text, sel->text, cursor); 621 text[cursor] = '\0'; 622 match(); 623 break; 624 } 625 626 draw: 627 drawmenu(); 628 } 629 630 static void 631 readstdin(void) 632 { 633 char *line = NULL; 634 size_t i, itemsiz = 0, linesiz = 0; 635 ssize_t len; 636 637 /* read each line from stdin and add it to the item list */ 638 for (i = 0; (len = getline(&line, &linesiz, stdin)) != -1; i++) { 639 if (i + 1 >= itemsiz) { 640 itemsiz += 256; 641 if (!(items = realloc(items, itemsiz * sizeof(*items)))) 642 die("cannot realloc %zu bytes:", itemsiz * sizeof(*items)); 643 } 644 if (line[len - 1] == '\n') 645 line[len - 1] = '\0'; 646 if (!(items[i].text = strdup(line))) 647 die("strdup:"); 648 items[i].out = 0; 649 } 650 free(line); 651 if (items) 652 items[i].text = NULL; 653 lines = MIN(lines, i); 654 } 655 656 static void 657 run(void) 658 { 659 drawmenu(); 660 for (;;) { 661 if (resized) 662 resize(); 663 keypress(readkey()); 664 } 665 } 666 667 static void 668 setup(void) 669 { 670 int i, termerr; 671 struct sigaction sa; 672 struct termios raw; 673 674 ttyfd = open("/dev/tty", O_RDWR | O_CLOEXEC); 675 if (ttyfd < 0) 676 ttyfd = dup(STDERR_FILENO); 677 if (ttyfd < 0) 678 die("open /dev/tty:"); 679 if (!(ttyout = fdopen(dup(ttyfd), "w"))) 680 die("fdopen:"); 681 if (setupterm(NULL, ttyfd, &termerr) == -1 || termerr <= 0) 682 die("setupterm failed"); 683 drw_term_init(ttyout); 684 685 if (tcgetattr(ttyfd, &termios_orig) == -1) 686 die("tcgetattr:"); 687 termios_saved = 1; 688 raw = termios_orig; 689 raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); 690 raw.c_oflag &= ~(OPOST); 691 raw.c_cflag |= (CS8); 692 raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); 693 raw.c_cc[VMIN] = 1; 694 raw.c_cc[VTIME] = 0; 695 if (tcsetattr(ttyfd, TCSAFLUSH, &raw) == -1) 696 die("tcsetattr:"); 697 698 /* init appearance */ 699 for (i = 0; i < SchemeLast; i++) 700 scheme[i] = drw_scm_create(drw, colors[i], 2); 701 702 memset(&sa, 0, sizeof sa); 703 sa.sa_handler = sigwinch; 704 sigemptyset(&sa.sa_mask); 705 sigaction(SIGWINCH, &sa, NULL); 706 match(); 707 drw_cursor_save(); 708 resize(); 709 } 710 711 static void 712 usage(void) 713 { 714 die("usage: tmenu [-biv] [-l lines] [-p prompt]\n" 715 " [-nb color] [-nf color] [-sb color] [-sf color]"); 716 } 717 718 int 719 main(int argc, char *argv[]) 720 { 721 int i; 722 723 for (i = 1; i < argc; i++) 724 /* these options take no arguments */ 725 if (!strcmp(argv[i], "-v")) { /* prints version information */ 726 puts("tmenu-"VERSION); 727 exit(0); 728 } else if (!strcmp(argv[i], "-b")) /* appears at the bottom of the screen */ 729 topbar = 0; 730 else if (!strcmp(argv[i], "-i")) { /* case-insensitive item matching */ 731 fstrncmp = strncasecmp; 732 fstrstr = cistrstr; 733 } else if (i + 1 == argc) 734 usage(); 735 /* these options take one argument */ 736 else if (!strcmp(argv[i], "-l")) /* number of lines in vertical list */ 737 lines = atoi(argv[++i]); 738 else if (!strcmp(argv[i], "-p")) /* adds prompt to left of input field */ 739 prompt = argv[++i]; 740 else if (!strcmp(argv[i], "-fn")) /* font or font set */ 741 fonts[0] = argv[++i]; 742 else if (!strcmp(argv[i], "-nb")) /* normal background color */ 743 colors[SchemeNorm][ColBg] = argv[++i]; 744 else if (!strcmp(argv[i], "-nf")) /* normal foreground color */ 745 colors[SchemeNorm][ColFg] = argv[++i]; 746 else if (!strcmp(argv[i], "-sb")) /* selected background color */ 747 colors[SchemeSel][ColBg] = argv[++i]; 748 else if (!strcmp(argv[i], "-sf")) /* selected foreground color */ 749 colors[SchemeSel][ColFg] = argv[++i]; 750 else if (!strcmp(argv[i], "-ob")) /* outline background color */ 751 colors[SchemeOut][ColBg] = argv[++i]; 752 else if (!strcmp(argv[i], "-of")) /* outline foreground color */ 753 colors[SchemeOut][ColFg] = argv[++i]; 754 else if (!strcmp(argv[i], "-f") || !strcmp(argv[i], "-m") || !strcmp(argv[i], "-w")) /* legacy compatibility */ 755 if (strcmp(argv[i], "-f") && ++i == argc) 756 usage(); 757 else 758 continue; 759 else 760 usage(); 761 762 if (!setlocale(LC_CTYPE, "")) 763 fputs("warning: no locale support\n", stderr); 764 passwd = !strcmp(colors[SchemeNorm][ColFg], colors[SchemeNorm][ColBg]); 765 766 drw = drw_create(NULL, 0, 0, 0, 0); 767 if (!drw_fontset_create(drw, fonts, LENGTH(fonts))) 768 die("no fonts could be loaded."); 769 lrpad = drw->fonts->h; 770 771 #ifdef __OpenBSD__ 772 if (pledge("stdio rpath tty", NULL) == -1) 773 die("pledge"); 774 #endif 775 776 readstdin(); 777 setup(); 778 run(); 779 780 return 1; /* unreachable */ 781 }