├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── config.mk ├── slmenu.1 └── slmenu.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.exe 3 | tags 4 | slmenu 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT/X Consortium License 2 | 3 | © 2011 Rafael Garcia Gallego 4 | 5 | Based on dmenu: 6 | © 2010-2011 Connor Lane Smith 7 | © 2006-2011 Anselm R Garbe 8 | © 2009 Gottox 9 | © 2009 Markus Schnalke 10 | © 2009 Evan Gates 11 | © 2006-2008 Sander van Dijk 12 | © 2006-2007 Michał Janeczek 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a 15 | copy of this software and associated documentation files (the "Software"), 16 | to deal in the Software without restriction, including without limitation 17 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 18 | and/or sell copies of the Software, and to permit persons to whom the 19 | Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in 22 | all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 27 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 29 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 30 | DEALINGS IN THE SOFTWARE. 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # slmenu - single line menu 2 | # See LICENSE file for copyright and license details. 3 | 4 | include config.mk 5 | 6 | SRC = slmenu.c 7 | OBJ = ${SRC:.c=.o} 8 | 9 | all: slmenu 10 | 11 | options: 12 | @echo slmenu build options: 13 | @echo "CFLAGS = ${CFLAGS}" 14 | @echo "LDFLAGS = ${LDFLAGS}" 15 | @echo "CC = ${CC}" 16 | 17 | .c.o: 18 | @echo CC -c $< 19 | @${CC} -c $< ${CFLAGS} 20 | 21 | ${OBJ}: config.mk 22 | 23 | slmenu: slmenu.o 24 | @echo CC -o $@ 25 | @${CC} -o $@ slmenu.o ${LDFLAGS} 26 | 27 | clean: 28 | @echo cleaning 29 | @rm -f slmenu ${OBJ} slmenu-${VERSION}.tar.gz 30 | 31 | dist: clean 32 | @echo creating dist tarball 33 | @mkdir -p slmenu-${VERSION} 34 | @cp LICENSE Makefile README config.mk slmenu.1 ${SRC} slmenu-${VERSION} 35 | @tar -cf slmenu-${VERSION}.tar slmenu-${VERSION} 36 | @gzip slmenu-${VERSION}.tar 37 | @rm -rf slmenu-${VERSION} 38 | 39 | install: all 40 | @echo installing executables to ${DESTDIR}${PREFIX}/bin 41 | @mkdir -p ${DESTDIR}${PREFIX}/bin 42 | @cp -f slmenu ${DESTDIR}${PREFIX}/bin 43 | @chmod 755 ${DESTDIR}${PREFIX}/bin/slmenu 44 | @echo installing manual pages to ${DESTDIR}${MANPREFIX}/man1 45 | @mkdir -p ${DESTDIR}${MANPREFIX}/man1 46 | @sed "s/VERSION/${VERSION}/g" < slmenu.1 > ${DESTDIR}${MANPREFIX}/man1/slmenu.1 47 | @chmod 644 ${DESTDIR}${MANPREFIX}/man1/slmenu.1 48 | 49 | uninstall: 50 | @echo removing executables from ${DESTDIR}${PREFIX}/bin 51 | @rm -f ${DESTDIR}${PREFIX}/bin/slmenu 52 | @echo removing manual page from ${DESTDIR}${MANPREFIX}/man1 53 | @rm -f ${DESTDIR}${MANPREFIX}/man1/slmenu.1 54 | 55 | .PHONY: all options clean dist install uninstall 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | slmenu - single line menu 2 | ========================= 3 | slmenu is a dmenu clone for the console. 4 | 5 | 6 | Requirements 7 | ------------ 8 | Just a C compiler that supports _BSD_SOURCE 9 | 10 | 11 | Installation 12 | ------------ 13 | Edit config.mk to match your local setup (slmenu is installed into 14 | the /usr/local namespace by default). 15 | 16 | Afterwards enter the following command to build and install slmenu 17 | (if necessary as root): 18 | 19 | make clean install 20 | 21 | 22 | Running slmenu 23 | ------------- 24 | See the man page for details. 25 | -------------------------------------------------------------------------------- /config.mk: -------------------------------------------------------------------------------- 1 | # slmenu version 2 | VERSION = 0.1 3 | 4 | # paths 5 | PREFIX = /usr/local 6 | MANPREFIX = ${PREFIX}/share/man 7 | 8 | # flags 9 | CPPFLAGS = -D_DEFAULT_SOURCE -DVERSION=\"${VERSION}\" 10 | CFLAGS = -ansi -pedantic -Wall -Os ${CPPFLAGS} 11 | LDFLAGS = -s 12 | 13 | # compiler and linker 14 | CC = cc 15 | -------------------------------------------------------------------------------- /slmenu.1: -------------------------------------------------------------------------------- 1 | .TH SLMENU 1 slmenu\-VERSION 2 | .SH NAME 3 | slmenu \- single line menu 4 | .SH SYNOPSIS 5 | .B slmenu 6 | .RB [ \-b ] 7 | .RB [ \-t ] 8 | .RB [ \-i ] 9 | .RB [ \-l 10 | .IR lines ] 11 | .RB [ \-p 12 | .IR prompt ] 13 | .RB [ \-v ] 14 | .SH DESCRIPTION 15 | .B slmenu 16 | is a dynamic menu for the console, based on the wonderful 17 | .IR dmenu (1). 18 | It also manages huge numbers of user\-defined menu items efficiently. 19 | .P 20 | slmenu reads a list of newline\-separated items from stdin and creates a menu. 21 | Then it re-opens the tty and prompts for user input. 22 | When the user selects an item or enters any text and presses Return, their 23 | choice is printed to stdout and slmenu terminates. 24 | The user interface is drawn in stderr, so it does not interfere with the 25 | result. 26 | .SH OPTIONS 27 | .TP 28 | .B \-b " or " -t 29 | slmenu appears at the bottom (\-b) or top (\-t) of the terminal. By default, 30 | slmenu appears in the current line. 31 | .TP 32 | .B \-i 33 | slmenu matches menu items case insensitively. 34 | .TP 35 | .BI \-l " lines" 36 | puts slmenu in multiline mode and chooses the number of lines to display 37 | .TP 38 | .BI \-p " prompt" 39 | defines the prompt to be displayed to the left of the input field. 40 | .TP 41 | .B \-v 42 | prints version information to stdout, then exits. 43 | .SH USAGE 44 | slmenu is completely controlled by the keyboard. Besides standard Unix line 45 | editing and item selection (arrow keys, page up/down, home and end), the 46 | following keys are recognized: 47 | .TP 48 | .B Tab (Ctrl\-i) 49 | Select the next entry in the list of matches. 50 | .TP 51 | .B Shift-Tab 52 | Select the previous entry in the list of matches. 53 | .TP 54 | .B Return (Ctrl\-j " or " Ctrl\-m) 55 | Confirm selection. Prints the selected item to stdout and exits, returning 56 | success. 57 | .TP 58 | .B Ctrl\-\] " or " Ctrl\-\\\\ 59 | Confirm input. Prints the input text to stdout and exits, returning success. 60 | .TP 61 | .B Escape\-Escape (Ctrl\-c) 62 | Exit without selecting an item, returning failure. 63 | .SH SEE ALSO 64 | .IR dmenu (1), 65 | .IR sandy (1), 66 | -------------------------------------------------------------------------------- /slmenu.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #define CONTROL(ch) ((ch) ^ 0x40) 14 | #define MIN(a,b) ((a) < (b) ? (a) : (b)) 15 | #define MAX(a,b) ((a) > (b) ? (a) : (b)) 16 | #define FALSE 0 17 | #define TRUE 1 18 | 19 | #define HIGHLIGHT_SEQ "\033[7m" 20 | #define DEBUG_SEQ "\033[101m" 21 | 22 | enum Color { 23 | Normal, 24 | Highlight, 25 | Debug 26 | }; 27 | typedef enum Color Color; 28 | 29 | typedef struct Item Item; 30 | struct Item { 31 | char *text; 32 | Item *left, *right; 33 | }; 34 | 35 | static void appenditem(Item*, Item**, Item**); 36 | static void calcoffsets(void); 37 | static void cleanup(void); 38 | static void die(const char*); 39 | static void drawtext(const char*, size_t, Color); 40 | static void drawmenu(void); 41 | static char *fstrstr(const char*, const char*); 42 | static void insert(const char*, ssize_t); 43 | static void match(int); 44 | static size_t nextrune(int); 45 | static void readstdin(void); 46 | static int run(void); 47 | static void resetline(void); 48 | static void setup(void); 49 | static size_t textw(const char*); 50 | static size_t textwn(const char*, int); 51 | 52 | static char text[BUFSIZ] = ""; 53 | static int barpos = 0; 54 | static int mw, mh; 55 | static int lines = 0; 56 | static int inputw, promptw; 57 | static size_t cursor; 58 | static char *prompt = NULL; 59 | static Item *items = NULL; 60 | static Item *matches, *matchend; 61 | static Item *prev, *curr, *next, *sel; 62 | static struct termios tio_old, tio_new; 63 | static int (*fstrncmp)(const char *, const char *, size_t) = strncmp; 64 | 65 | void appenditem(Item *item, Item **list, Item **last) { 66 | if (!*last) { 67 | *list = item; 68 | } else { 69 | (*last)->right = item; 70 | } 71 | item->left = *last; 72 | item->right = NULL; 73 | *last = item; 74 | } 75 | 76 | void calcoffsets(void) { 77 | int i, n; 78 | 79 | if (lines > 0) { 80 | n = lines; 81 | } else { 82 | n = mw - (promptw + inputw + textw("<") + textw(">")); 83 | 84 | for (i = 0, next = curr; next; next = next->right) { 85 | i += (lines>0 ? 1 : MIN(textw(next->text), n)); 86 | if (i > n) { 87 | break; 88 | } 89 | } 90 | 91 | for (i = 0, prev = curr; prev && prev->left; prev = prev->left) { 92 | i += (lines>0 ? 1 : MIN(textw(prev->left->text), n)); 93 | if (i > n) { 94 | break; 95 | } 96 | } 97 | } 98 | } 99 | 100 | void cleanup() { 101 | if (barpos==0) { 102 | fprintf(stderr, "\n"); 103 | } else { 104 | fprintf(stderr, "\033[G\033[K"); 105 | } 106 | tcsetattr(0, TCSANOW, &tio_old); 107 | } 108 | 109 | void die(const char *s) { 110 | tcsetattr(0, TCSANOW, &tio_old); 111 | fprintf(stderr, "%s\n", s); 112 | exit(1); 113 | } 114 | 115 | void drawtext(const char *t, size_t w, Color col) { 116 | const char *prestr, *poststr; 117 | int i, tw; 118 | char *buf; 119 | 120 | if (w<3) { 121 | /* This is the minimum size needed to write a label: 1 char + 2 padding spaces */ 122 | return; 123 | } 124 | 125 | tw = w-2; /* This is the text width, without the padding */ 126 | buf = calloc(1, (tw+1)); 127 | if (buf == NULL) { 128 | die("Can't calloc."); 129 | } 130 | 131 | switch(col) { 132 | case Debug: 133 | prestr = DEBUG_SEQ; 134 | poststr = "\033[0m"; 135 | break; 136 | case Highlight: 137 | prestr = HIGHLIGHT_SEQ; 138 | poststr = "\033[0m"; 139 | break; 140 | case Normal: 141 | default: 142 | prestr=poststr=""; 143 | } 144 | 145 | memset(buf, ' ', tw); 146 | buf[tw] = '\0'; 147 | memcpy(buf, t, MIN(strlen(t), tw)); 148 | if (textw(t) > w) { 149 | /* Remember textw returns the width WITH padding */ 150 | for (i=MAX((tw-2), 0); i 0) { 177 | if (barpos != 0) { 178 | resetline(); 179 | } 180 | 181 | for (rw=0, item=curr; item!=next && rwright) { 182 | fprintf(stderr, "\n"); 183 | drawtext(item->text, mw, (item == sel) ? Highlight : Normal); 184 | } 185 | 186 | for(; rwleft) { 195 | drawtext("<", 3 /*textw("<")*/, Normal); 196 | } 197 | 198 | for (item = curr; item != next; item = item->right) { 199 | /* item width */ 200 | int iw = MIN(textw(item->text), rw); 201 | drawtext(item->text, iw, (item == sel) ? Highlight : Normal); 202 | 203 | rw -= textw(item->text); 204 | if (rw <= 0) 205 | break; 206 | } 207 | if (next) { 208 | fprintf(stderr, "\033[%iG", mw-4); 209 | drawtext(" >", 5 /*textw(">")*/, Normal); 210 | } 211 | 212 | } 213 | fprintf(stderr, "\033[%iG", (int)(promptw+textwn(text, cursor)-1)); 214 | } 215 | 216 | char* 217 | fstrstr(const char *s, const char *sub) { 218 | size_t len; 219 | 220 | for(len = strlen(sub); *s; s++) 221 | if(!fstrncmp(s, sub, len)) 222 | return (char *)s; 223 | return NULL; 224 | } 225 | 226 | void insert(const char *str, ssize_t n) { 227 | if (strlen(text) + n > sizeof text - 1) { 228 | return; 229 | } 230 | memmove(&text[cursor + n], &text[cursor], sizeof text - cursor - MAX(n, 0)); 231 | if (n > 0) { 232 | memcpy(&text[cursor], str, n); 233 | } 234 | cursor += n; 235 | match(n > 0 && text[cursor] == '\0'); 236 | } 237 | 238 | void match(int sub) { 239 | size_t len = strlen(text); 240 | Item *lexact, *lprefix, *lsubstr, *exactend, *prefixend, *substrend; 241 | Item *item, *lnext; 242 | 243 | lexact = lprefix = lsubstr = exactend = prefixend = substrend = NULL; 244 | for(item = sub ? matches : items; item && item->text; item = lnext) { 245 | lnext = sub ? item->right : item + 1; 246 | if(!fstrncmp(text, item->text, len + 1)) 247 | appenditem(item, &lexact, &exactend); 248 | else if(!fstrncmp(text, item->text, len)) 249 | appenditem(item, &lprefix, &prefixend); 250 | else if(fstrstr(item->text, text)) 251 | appenditem(item, &lsubstr, &substrend); 252 | } 253 | matches = lexact; 254 | matchend = exactend; 255 | 256 | if(lprefix) { 257 | if(matchend) { 258 | matchend->right = lprefix; 259 | lprefix->left = matchend; 260 | } 261 | else 262 | matches = lprefix; 263 | matchend = prefixend; 264 | } 265 | if(lsubstr) { 266 | if(matchend) { 267 | matchend->right = lsubstr; 268 | lsubstr->left = matchend; 269 | } 270 | else 271 | matches = lsubstr; 272 | matchend = substrend; 273 | } 274 | curr = sel = matches; 275 | calcoffsets(); 276 | } 277 | 278 | size_t nextrune(int inc) { 279 | ssize_t n; 280 | 281 | for(n = cursor + inc; n + inc >= 0 && (text[n] & 0xc0) == 0x80; n += inc); 282 | return n; 283 | } 284 | 285 | void readstdin() { 286 | char buf[sizeof text], *p; 287 | size_t i, str_len, max = 10, size = 0; 288 | 289 | for (i = 0; fgets(buf, sizeof buf, stdin); i++) { 290 | 291 | if (i+1 >= size / sizeof *items) { 292 | size += BUFSIZ; 293 | items = realloc(items, size); 294 | if (!items) { 295 | die("Can't realloc."); 296 | } 297 | } 298 | 299 | p = strchr(buf, '\n'); 300 | if (p) { 301 | *p = '\0'; 302 | } 303 | 304 | items[i].text = strdup(buf); 305 | if (!items[i].text) { 306 | die("Can't strdup."); 307 | } 308 | 309 | /* Get longest item */ 310 | str_len = textw(items[i].text); 311 | if (str_len > max) { 312 | max = str_len; 313 | } 314 | } 315 | 316 | if (items) { 317 | items[i].text = NULL; 318 | } 319 | 320 | inputw = max; 321 | } 322 | 323 | void resetline(void) { 324 | if (barpos != 0) { 325 | fprintf(stderr, "\033[%iH", (barpos>0) ? 0 : (mh-lines)); 326 | } else { 327 | fprintf(stderr, "\033[%iF", lines); 328 | } 329 | } 330 | 331 | int run(void) { 332 | char buf[32]; 333 | char c; 334 | 335 | while (1) { 336 | read(0, &c, 1); 337 | memset(buf, '\0', sizeof buf); 338 | buf[0]=c; 339 | switch_top: 340 | switch (c) { 341 | case CONTROL('['): 342 | read(0, &c, 1); 343 | esc_switch_top: 344 | switch (c) { 345 | case CONTROL('['): 346 | /* ESC, need to press twice due to console limitations */ 347 | c=CONTROL('C'); 348 | goto switch_top; 349 | case '[': 350 | read(0, &c, 1); 351 | switch(c) { 352 | case '1': /* Home */ 353 | case '7': 354 | case 'H': 355 | if (c!='H') { 356 | /* Remove trailing '~' from stdin */ 357 | read(0, &c, 1); 358 | } 359 | c=CONTROL('A'); 360 | goto switch_top; 361 | case '2': /* Insert */ 362 | read(0, &c, 1); /* Remove trailing '~' from stdin */ 363 | c=CONTROL('Y'); 364 | goto switch_top; 365 | case '3': /* Delete */ 366 | read(0, &c, 1); /* Remove trailing '~' from stdin */ 367 | c=CONTROL('D'); 368 | goto switch_top; 369 | case '4': /* End */ 370 | case '8': 371 | case 'F': 372 | if (c!='F') { 373 | /* Remove trailing '~' from stdin */ 374 | read(0, &c, 1); 375 | } 376 | c=CONTROL('E'); 377 | goto switch_top; 378 | case '5': /* PageUp */ 379 | read(0, &c, 1); /* Remove trailing '~' from stdin */ 380 | c=CONTROL('V'); 381 | goto switch_top; 382 | case '6': /* PageDown */ 383 | read(0, &c, 1); /* Remove trailing '~' from stdin */ 384 | c='v'; 385 | goto esc_switch_top; 386 | case 'A': /* Up arrow */ 387 | c=CONTROL('P'); 388 | goto switch_top; 389 | case 'B': /* Down arrow */ 390 | c=CONTROL('N'); 391 | goto switch_top; 392 | case 'C': /* Right arrow */ 393 | c=CONTROL('F'); 394 | goto switch_top; 395 | case 'D': /* Left arrow */ 396 | c=CONTROL('B'); 397 | goto switch_top; 398 | case 'Z': /* Shift-TAB */ 399 | c=CONTROL('P'); 400 | goto switch_top; 401 | } 402 | break; 403 | case 'b': 404 | while (cursor > 0 && text[nextrune(-1)] == ' ') { 405 | cursor = nextrune(-1); 406 | } 407 | while (cursor > 0 && text[nextrune(-1)] != ' ') { 408 | cursor = nextrune(-1); 409 | } 410 | break; 411 | case 'f': 412 | while (text[cursor] != '\0' && text[nextrune(+1)] == ' ') { 413 | cursor = nextrune(+1); 414 | } 415 | if (text[cursor] != '\0') { 416 | do { 417 | cursor = nextrune(+1); 418 | } while(text[cursor] != '\0' && text[cursor] != ' '); 419 | } 420 | break; 421 | case 'd': 422 | while (text[cursor] != '\0' && text[nextrune(+1)] == ' ') { 423 | cursor = nextrune(+1); 424 | insert(NULL, nextrune(-1) - cursor); 425 | } 426 | if (text[cursor] != '\0') { 427 | do { 428 | cursor = nextrune(+1); 429 | insert(NULL, nextrune(-1) - cursor); 430 | } while(text[cursor] != '\0' && text[cursor] != ' '); 431 | } 432 | break; 433 | case 'v': 434 | if (!next) { 435 | break; 436 | } 437 | sel=curr=next; 438 | calcoffsets(); 439 | break; 440 | default: 441 | break; 442 | } 443 | break; 444 | case CONTROL('C'): 445 | /* cancel */ 446 | return EXIT_FAILURE; 447 | case CONTROL('M'): /* Return */ 448 | case CONTROL('J'): 449 | if (sel) { 450 | /* Complete the input first, when hitting return */ 451 | strncpy(text, sel->text, sizeof text); 452 | } 453 | cursor = strlen(text); 454 | match(TRUE); 455 | drawmenu(); 456 | /* fallthrough */ 457 | case CONTROL(']'): 458 | case CONTROL('\\'): 459 | /* These are usually close enough to RET to replace Shift+RET, 460 | * again due to console limitations */ 461 | puts(text); 462 | return EXIT_SUCCESS; 463 | case CONTROL('A'): 464 | /* cursor to start of line */ 465 | if (sel == matches) { 466 | cursor=0; 467 | break; 468 | } 469 | sel=curr=matches; 470 | calcoffsets(); 471 | break; 472 | case CONTROL('E'): 473 | /* cursor to end of line */ 474 | if (text[cursor] != '\0') { 475 | cursor = strlen(text); 476 | break; 477 | } 478 | if (next) { 479 | curr = matchend; 480 | calcoffsets(); 481 | curr = prev; 482 | calcoffsets(); 483 | while (next && (curr = curr->right)) { 484 | calcoffsets(); 485 | } 486 | } 487 | sel = matchend; 488 | break; 489 | case CONTROL('B'): 490 | /* cursor back 1 character */ 491 | if (cursor > 0 && (!sel || !sel->left || lines > 0)) { 492 | cursor = nextrune(-1); 493 | break; 494 | } 495 | /* fallthrough */ 496 | case CONTROL('P'): 497 | /* select previous entry */ 498 | if (sel && sel->left && (sel = sel->left)->right == curr) { 499 | curr = prev; 500 | calcoffsets(); 501 | } 502 | break; 503 | case CONTROL('F'): 504 | /* cursor forward 1 character */ 505 | if (text[cursor] != '\0') { 506 | cursor = nextrune(+1); 507 | break; 508 | } 509 | /* fallthrough */ 510 | case CONTROL('N'): 511 | /* select next entry */ 512 | if (sel && sel->right && (sel = sel->right) == next) { 513 | curr = next; 514 | calcoffsets(); 515 | } 516 | break; 517 | case CONTROL('D'): 518 | /* delete character under cursor */ 519 | if (text[cursor] == '\0') { 520 | break; 521 | } 522 | cursor = nextrune(+1); 523 | /* fallthrough */ 524 | case CONTROL('H'): 525 | case CONTROL('?'): /* Backspace */ 526 | /* delete character before cursor */ 527 | if (cursor == 0) { 528 | break; 529 | } 530 | insert(NULL, nextrune(-1) - cursor); 531 | break; 532 | case CONTROL('I'): /* TAB */ 533 | c=CONTROL('F'); 534 | goto switch_top; 535 | case CONTROL('K'): 536 | /* delete everything under cursor to end */ 537 | text[cursor] = '\0'; 538 | match(FALSE); 539 | break; 540 | case CONTROL('U'): 541 | /* delete everything from start to before cursor */ 542 | insert(NULL, 0 - cursor); 543 | break; 544 | case CONTROL('W'): 545 | /* delete from before cursor back to space */ 546 | while (cursor > 0 && text[nextrune(-1)] == ' ') { 547 | insert(NULL, nextrune(-1) - cursor); 548 | } 549 | while (cursor > 0 && text[nextrune(-1)] != ' ') { 550 | insert(NULL, nextrune(-1) - cursor); 551 | } 552 | break; 553 | case CONTROL('V'): 554 | /* jump selection backwards */ 555 | if (!prev) { 556 | break; 557 | } 558 | sel = curr = prev; 559 | calcoffsets(); 560 | break; 561 | default: 562 | if (!iscntrl(*buf)) { 563 | insert(buf, strlen(buf)); 564 | } 565 | break; 566 | } 567 | drawmenu(); 568 | } 569 | } 570 | 571 | void setup(void) { 572 | int fd, result=-1; 573 | struct winsize ws; 574 | 575 | /* re-open stdin to read keyboard */ 576 | if (freopen("/dev/tty", "r", stdin) == NULL) { 577 | die("Can't reopen tty."); 578 | } 579 | 580 | /* ioctl() the tty to get size */ 581 | fd = open("/dev/tty", O_RDWR); 582 | if (fd == -1) { 583 | mw = 80; 584 | mh = 24; 585 | } else { 586 | result = ioctl(fd, TIOCGWINSZ, &ws); 587 | close(fd); 588 | if (result<0) { 589 | mw = 80; 590 | mh = 24; 591 | } else { 592 | mw = ws.ws_col; 593 | mh = ws.ws_row; 594 | } 595 | } 596 | 597 | /* change terminal attributes, save old */ 598 | tcgetattr(0, &tio_old); 599 | memcpy ((char *)&tio_new, (char *)&tio_old, sizeof(struct termios)); 600 | tio_new.c_iflag &= ~(BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON); 601 | tio_new.c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN); 602 | tio_new.c_cflag &= ~(CSIZE|PARENB); 603 | tio_new.c_cflag |= CS8; 604 | tio_new.c_cc[VMIN]=1; 605 | tcsetattr(0, TCSANOW, &tio_new); 606 | 607 | lines = MIN(MAX(lines, 0), mh-1); 608 | promptw = (prompt?textw(prompt):0); 609 | 610 | /* text input area */ 611 | inputw = MIN(inputw, mw/6); 612 | 613 | match(FALSE); 614 | if (barpos!=0) { 615 | resetline(); 616 | } 617 | } 618 | 619 | size_t textw(const char *s) { 620 | return textwn(s, -1); 621 | } 622 | 623 | size_t textwn(const char *s, int l) { 624 | int b, c; /* bytes and UTF-8 characters */ 625 | 626 | for (b=c=0; s && s[b] && (l<0 || b