vis: normalize selections after pairwise combination
[vis.git] / vis-menu.c
blob1682ed2637dc31765215e3e875b826514e7ca143
1 /*
2 * MIT/X Consortium License
4 * © 2011 Rafael Garcia Gallego <rafael.garcia.gallego@gmail.com>
6 * Based on dmenu:
7 * © 2010-2011 Connor Lane Smith <cls@lubutu.com>
8 * © 2006-2011 Anselm R Garbe <anselm@garbe.us>
9 * © 2009 Gottox <gottox@s01.de>
10 * © 2009 Markus Schnalke <meillo@marmaro.de>
11 * © 2009 Evan Gates <evan.gates@gmail.com>
12 * © 2006-2008 Sander van Dijk <a dot h dot vandijk at gmail dot com>
13 * © 2006-2007 Michał Janeczek <janeczek at gmail dot com>
15 * Permission is hereby granted, free of charge, to any person obtaining a
16 * copy of this software and associated documentation files (the "Software"),
17 * to deal in the Software without restriction, including without limitation
18 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
19 * and/or sell copies of the Software, and to permit persons to whom the
20 * Software is furnished to do so, subject to the following conditions:
22 * The above copyright notice and this permission notice shall be included in
23 * all copies or substantial portions of the Software.
25 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
28 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
30 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
31 * DEALINGS IN THE SOFTWARE.
33 #include <fcntl.h>
34 #include <stdio.h>
35 #include <stdlib.h>
36 #include <string.h>
37 #include <strings.h>
38 #include <ctype.h>
39 #include <sys/ioctl.h>
40 #include <sys/stat.h>
41 #include <sys/types.h>
42 #include <termios.h>
43 #include <unistd.h>
45 #define CONTROL(ch) (ch ^ 0x40)
46 #define MIN(a,b) ((a) < (b) ? (a) : (b))
47 #define MAX(a,b) ((a) > (b) ? (a) : (b))
49 typedef enum {
50 C_Normal,
51 C_Reverse
52 } Color;
54 typedef struct Item Item;
55 struct Item {
56 char *text;
57 Item *left, *right;
60 static char text[BUFSIZ] = "";
61 static int barpos = 0;
62 static int mw, mh;
63 static int lines = 0;
64 static int inputw, promptw;
65 static size_t cursor;
66 static char *prompt = NULL;
67 static Item *items = NULL;
68 static Item *matches, *matchend;
69 static Item *prev, *curr, *next, *sel;
70 static struct termios tio_old, tio_new;
71 static int (*fstrncmp)(const char *, const char *, size_t) = strncmp;
73 static void
74 appenditem(Item *item, Item **list, Item **last) {
75 if (!*last)
76 *list = item;
77 else
78 (*last)->right = item;
79 item->left = *last;
80 item->right = NULL;
81 *last = item;
84 static size_t
85 textwn(const char *s, int l) {
86 int b, c; /* bytes and UTF-8 characters */
88 for(b=c=0; s && s[b] && (l<0 || b<l); b++) if((s[b] & 0xc0) != 0x80) c++;
89 return c+4; /* Accomodate for the leading and trailing spaces */
92 static size_t
93 textw(const char *s) {
94 return textwn(s, -1);
97 static void
98 calcoffsets(void) {
99 int i, n;
101 if (lines > 0)
102 n = lines;
103 else
104 n = mw - (promptw + inputw + textw("<") + textw(">"));
106 for (i = 0, next = curr; next; next = next->right)
107 if ((i += (lines>0 ? 1 : MIN(textw(next->text), n))) > n)
108 break;
109 for (i = 0, prev = curr; prev && prev->left; prev = prev->left)
110 if ((i += (lines>0 ? 1 : MIN(textw(prev->left->text), n))) > n)
111 break;
114 static void
115 cleanup() {
116 if (barpos == 0) fprintf(stderr, "\n");
117 else fprintf(stderr, "\033[G\033[K");
118 tcsetattr(0, TCSANOW, &tio_old);
121 static void
122 die(const char *s) {
123 tcsetattr(0, TCSANOW, &tio_old);
124 fprintf(stderr, "%s\n", s);
125 exit(EXIT_FAILURE);
128 static void
129 drawtext(const char *t, size_t w, Color col) {
130 const char *prestr, *poststr;
131 int i, tw;
132 char *buf;
134 if (w<5) return; /* This is the minimum size needed to write a label: 1 char + 4 padding spaces */
135 tw = w-4; /* This is the text width, without the padding */
136 if (!(buf = calloc(1, tw+1))) die("Can't calloc.");
137 switch (col) {
138 case C_Reverse:
139 prestr="\033[7m";
140 poststr="\033[0m";
141 break;
142 case C_Normal:
143 default:
144 prestr=poststr="";
147 memset(buf, ' ', tw);
148 buf[tw] = '\0';
149 memcpy(buf, t, MIN(strlen(t), tw));
150 if (textw(t) > w) /* Remember textw returns the width WITH padding */
151 for (i = MAX((tw-4), 0); i < tw; i++) buf[i] = '.';
153 fprintf(stderr, "%s %s %s", prestr, buf, poststr);
154 free(buf);
157 static void
158 resetline(void) {
159 if (barpos != 0) fprintf(stderr, "\033[%iH", barpos > 0 ? 0 : (mh-lines));
160 else fprintf(stderr, "\033[%iF", lines);
163 static void
164 drawmenu(void) {
165 Item *item;
166 int rw;
168 /* use default colors */
169 fprintf(stderr, "\033[0m");
171 /* place cursor in first column, clear it */
172 fprintf(stderr, "\033[0G");
173 fprintf(stderr, "\033[K");
175 if (prompt)
176 drawtext(prompt, promptw, C_Reverse);
178 drawtext(text, (lines==0 && matches) ? inputw : mw-promptw, C_Normal);
180 if (lines > 0) {
181 if (barpos != 0) resetline();
182 for (rw = 0, item = curr; item != next; rw++, item = item->right) {
183 fprintf(stderr, "\n");
184 drawtext(item->text, mw, (item == sel) ? C_Reverse : C_Normal);
186 for (; rw < lines; rw++)
187 fprintf(stderr, "\n\033[K");
188 resetline();
189 } else if (matches) {
190 rw = mw-(4+promptw+inputw);
191 if (curr->left)
192 drawtext("<", 5 /*textw("<")*/, C_Normal);
193 for (item = curr; item != next; item = item->right) {
194 drawtext(item->text, MIN(textw(item->text), rw), (item == sel) ? C_Reverse : C_Normal);
195 if ((rw -= textw(item->text)) <= 0) break;
197 if (next) {
198 fprintf(stderr, "\033[%iG", mw-5);
199 drawtext(">", 5 /*textw(">")*/, C_Normal);
203 fprintf(stderr, "\033[%iG", (int)(promptw+textwn(text, cursor)-1));
204 fflush(stderr);
207 static char*
208 fstrstr(const char *s, const char *sub) {
209 for (size_t len = strlen(sub); *s; s++)
210 if (!fstrncmp(s, sub, len))
211 return (char*)s;
212 return NULL;
215 static void
216 match(void)
218 static char **tokv = NULL;
219 static int tokn = 0;
221 char buf[sizeof text], *s;
222 int i, tokc = 0;
223 size_t len, textsize;
224 Item *item, *lprefix, *lsubstr, *prefixend, *substrend;
226 strcpy(buf, text);
227 /* separate input text into tokens to be matched individually */
228 for (s = strtok(buf, " "); s; tokv[tokc - 1] = s, s = strtok(NULL, " "))
229 if (++tokc > tokn && !(tokv = realloc(tokv, ++tokn * sizeof *tokv)))
230 die("Can't realloc.");
231 len = tokc ? strlen(tokv[0]) : 0;
233 matches = lprefix = lsubstr = matchend = prefixend = substrend = NULL;
234 textsize = strlen(text) + 1;
235 for (item = items; item && item->text; item++) {
236 for (i = 0; i < tokc; i++)
237 if (!fstrstr(item->text, tokv[i]))
238 break;
239 if (i != tokc) /* not all tokens match */
240 continue;
241 /* exact matches go first, then prefixes, then substrings */
242 if (!tokc || !fstrncmp(text, item->text, textsize))
243 appenditem(item, &matches, &matchend);
244 else if (!fstrncmp(tokv[0], item->text, len))
245 appenditem(item, &lprefix, &prefixend);
246 else
247 appenditem(item, &lsubstr, &substrend);
249 if (lprefix) {
250 if (matches) {
251 matchend->right = lprefix;
252 lprefix->left = matchend;
253 } else
254 matches = lprefix;
255 matchend = prefixend;
257 if (lsubstr) {
258 if (matches) {
259 matchend->right = lsubstr;
260 lsubstr->left = matchend;
261 } else
262 matches = lsubstr;
263 matchend = substrend;
265 curr = sel = matches;
266 calcoffsets();
269 static void
270 insert(const char *str, ssize_t n) {
271 if (strlen(text) + n > sizeof text - 1)
272 return;
273 memmove(&text[cursor + n], &text[cursor], sizeof text - cursor - MAX(n, 0));
274 if (n > 0)
275 memcpy(&text[cursor], str, n);
276 cursor += n;
277 match();
280 static size_t
281 nextrune(int inc) {
282 ssize_t n;
284 for(n = cursor + inc; n + inc >= 0 && (text[n] & 0xc0) == 0x80; n += inc);
285 return n;
288 static void
289 readstdin() {
290 char buf[sizeof text], *p, *maxstr = NULL;
291 size_t i, max = 0, size = 0;
293 for(i = 0; fgets(buf, sizeof buf, stdin); i++) {
294 if (i+1 >= size / sizeof *items)
295 if (!(items = realloc(items, (size += BUFSIZ))))
296 die("Can't realloc.");
297 if ((p = strchr(buf, '\n')))
298 *p = '\0';
299 if (!(items[i].text = strdup(buf)))
300 die("Can't strdup.");
301 if (strlen(items[i].text) > max)
302 max = textw(maxstr = items[i].text);
304 if (items)
305 items[i].text = NULL;
306 inputw = textw(maxstr);
309 static void
310 xread(int fd, void *buf, size_t nbyte) {
311 ssize_t r = read(fd, buf, nbyte);
312 if (r < 0 || (size_t)r != nbyte)
313 die("Can not read.");
316 static void
317 setup(void) {
318 int fd, result = -1;
319 struct winsize ws;
321 /* re-open stdin to read keyboard */
322 if (!freopen("/dev/tty", "r", stdin)) die("Can't reopen tty as stdin.");
323 if (!freopen("/dev/tty", "w", stderr)) die("Can't reopen tty as stderr.");
325 /* ioctl() the tty to get size */
326 fd = open("/dev/tty", O_RDWR);
327 if (fd == -1) {
328 mh = 24;
329 mw = 80;
330 } else {
331 result = ioctl(fd, TIOCGWINSZ, &ws);
332 close(fd);
333 if (result < 0) {
334 mw = 80;
335 mh = 24;
336 } else {
337 mw = ws.ws_col;
338 mh = ws.ws_row;
342 /* change terminal attributes, save old */
343 tcgetattr(0, &tio_old);
344 tio_new = tio_old;
345 tio_new.c_iflag &= ~(BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON);
346 tio_new.c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
347 tio_new.c_cflag &= ~(CSIZE|PARENB);
348 tio_new.c_cflag |= CS8;
349 tio_new.c_cc[VMIN] = 1;
350 tcsetattr(0, TCSANOW, &tio_new);
352 lines = MIN(MAX(lines, 0), mh);
353 promptw = prompt ? textw(prompt) : 0;
354 inputw = MIN(inputw, mw/3);
355 match();
356 if (barpos != 0) resetline();
357 drawmenu();
360 static int
361 run(void) {
362 char buf[32];
363 char c;
365 for (;;) {
366 xread(0, &c, 1);
367 memset(buf, '\0', sizeof buf);
368 buf[0] = c;
369 switch_top:
370 switch(c) {
371 case CONTROL('['):
372 xread(0, &c, 1);
373 esc_switch_top:
374 switch(c) {
375 case CONTROL('['): /* ESC, need to press twice due to console limitations */
376 c = CONTROL('C');
377 goto switch_top;
378 case '[':
379 xread(0, &c, 1);
380 switch(c) {
381 case '1': /* Home */
382 case '7':
383 case 'H':
384 if (c != 'H') xread(0, &c, 1); /* Remove trailing '~' from stdin */
385 c = CONTROL('A');
386 goto switch_top;
387 case '2': /* Insert */
388 xread(0, &c, 1); /* Remove trailing '~' from stdin */
389 c = CONTROL('Y');
390 goto switch_top;
391 case '3': /* Delete */
392 xread(0, &c, 1); /* Remove trailing '~' from stdin */
393 c = CONTROL('D');
394 goto switch_top;
395 case '4': /* End */
396 case '8':
397 case 'F':
398 if (c != 'F') xread(0, &c, 1); /* Remove trailing '~' from stdin */
399 c = CONTROL('E');
400 goto switch_top;
401 case '5': /* PageUp */
402 xread(0, &c, 1); /* Remove trailing '~' from stdin */
403 c = CONTROL('V');
404 goto switch_top;
405 case '6': /* PageDown */
406 xread(0, &c, 1); /* Remove trailing '~' from stdin */
407 c = 'v';
408 goto esc_switch_top;
409 case 'A': /* Up arrow */
410 c = CONTROL('P');
411 goto switch_top;
412 case 'B': /* Down arrow */
413 c = CONTROL('N');
414 goto switch_top;
415 case 'C': /* Right arrow */
416 c = CONTROL('F');
417 goto switch_top;
418 case 'D': /* Left arrow */
419 c = CONTROL('B');
420 goto switch_top;
422 break;
423 case 'b':
424 while (cursor > 0 && text[nextrune(-1)] == ' ')
425 cursor = nextrune(-1);
426 while (cursor > 0 && text[nextrune(-1)] != ' ')
427 cursor = nextrune(-1);
428 break;
429 case 'f':
430 while (text[cursor] != '\0' && text[nextrune(+1)] == ' ')
431 cursor = nextrune(+1);
432 if (text[cursor] != '\0') {
433 do {
434 cursor = nextrune(+1);
435 } while (text[cursor] != '\0' && text[cursor] != ' ');
437 break;
438 case 'd':
439 while (text[cursor] != '\0' && text[nextrune(+1)] == ' ') {
440 cursor = nextrune(+1);
441 insert(NULL, nextrune(-1) - cursor);
443 if (text[cursor] != '\0') {
444 do {
445 cursor = nextrune(+1);
446 insert(NULL, nextrune(-1) - cursor);
447 } while (text[cursor] != '\0' && text[cursor] != ' ');
449 break;
450 case 'v':
451 if (!next)
452 break;
453 sel = curr = next;
454 calcoffsets();
455 break;
456 default:
457 break;
459 break;
460 case CONTROL('C'):
461 return EXIT_FAILURE;
462 case CONTROL('M'): /* Return */
463 case CONTROL('J'):
464 if (sel) strncpy(text, sel->text, sizeof(text)-1); /* Complete the input first, when hitting return */
465 cursor = strlen(text);
466 match();
467 drawmenu();
468 /* fallthrough */
469 case CONTROL(']'):
470 case CONTROL('\\'): /* These are usually close enough to RET to replace Shift+RET, again due to console limitations */
471 puts(text);
472 return EXIT_SUCCESS;
473 case CONTROL('A'):
474 if (sel == matches) {
475 cursor = 0;
476 break;
478 sel = curr = matches;
479 calcoffsets();
480 break;
481 case CONTROL('E'):
482 if (text[cursor] != '\0') {
483 cursor = strlen(text);
484 break;
486 if (next) {
487 curr = matchend;
488 calcoffsets();
489 curr = prev;
490 calcoffsets();
491 while(next && (curr = curr->right))
492 calcoffsets();
494 sel = matchend;
495 break;
496 case CONTROL('B'):
497 if (cursor > 0 && (!sel || !sel->left || lines > 0)) {
498 cursor = nextrune(-1);
499 break;
501 /* fallthrough */
502 case CONTROL('P'):
503 if (sel && sel->left && (sel = sel->left)->right == curr) {
504 curr = prev;
505 calcoffsets();
507 break;
508 case CONTROL('F'):
509 if (text[cursor] != '\0') {
510 cursor = nextrune(+1);
511 break;
513 /* fallthrough */
514 case CONTROL('N'):
515 if (sel && sel->right && (sel = sel->right) == next) {
516 curr = next;
517 calcoffsets();
519 break;
520 case CONTROL('D'):
521 if (text[cursor] == '\0')
522 break;
523 cursor = nextrune(+1);
524 /* fallthrough */
525 case CONTROL('H'):
526 case CONTROL('?'): /* Backspace */
527 if (cursor == 0)
528 break;
529 insert(NULL, nextrune(-1) - cursor);
530 break;
531 case CONTROL('I'): /* TAB */
532 if (!sel)
533 break;
534 strncpy(text, sel->text, sizeof text);
535 cursor = strlen(text);
536 match();
537 break;
538 case CONTROL('K'):
539 text[cursor] = '\0';
540 match();
541 break;
542 case CONTROL('U'):
543 insert(NULL, 0 - cursor);
544 break;
545 case CONTROL('W'):
546 while (cursor > 0 && text[nextrune(-1)] == ' ')
547 insert(NULL, nextrune(-1) - cursor);
548 while (cursor > 0 && text[nextrune(-1)] != ' ')
549 insert(NULL, nextrune(-1) - cursor);
550 break;
551 case CONTROL('V'):
552 if (!prev)
553 break;
554 sel = curr = prev;
555 calcoffsets();
556 break;
557 default:
558 if (!iscntrl(*buf))
559 insert(buf, strlen(buf));
560 break;
562 drawmenu();
566 static void
567 usage(void) {
568 fputs("usage: vis-menu [-b|-t] [-i] [-l lines] [-p prompt] [initial selection]\n", stderr);
569 exit(EXIT_FAILURE);
573 main(int argc, char **argv) {
574 for (int i = 1; i < argc; i++) {
575 if (!strcmp(argv[i], "-v")) {
576 puts("vis-menu " VERSION);
577 exit(EXIT_SUCCESS);
578 } else if (!strcmp(argv[i], "-i")) {
579 fstrncmp = strncasecmp;
580 } else if (!strcmp(argv[i], "-t")) {
581 barpos = +1;
582 } else if (!strcmp(argv[i], "-b")) {
583 barpos = -1;
584 } else if (argv[i][0] != '-') {
585 strncpy(text, argv[i], sizeof(text)-1);
586 cursor = strlen(text);
587 } else if (i + 1 == argc) {
588 usage();
589 } else if (!strcmp(argv[i], "-p")) {
590 prompt = argv[++i];
591 if (prompt && !prompt[0])
592 prompt = NULL;
593 } else if (!strcmp(argv[i], "-l")) {
594 lines = atoi(argv[++i]);
595 } else {
596 usage();
600 readstdin();
601 setup();
602 int status = run();
603 cleanup();
604 return status;