filetype: Set "groovy" for Jenkinsfile
[vis.git] / vis-menu.c
blob946de5686c76701a5077a2e8e32e388ffd007399
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>
44 #include <errno.h>
46 #define CONTROL(ch) (ch ^ 0x40)
47 #define MIN(a,b) ((a) < (b) ? (a) : (b))
48 #define MAX(a,b) ((a) > (b) ? (a) : (b))
50 typedef enum {
51 C_Normal,
52 C_Reverse
53 } Color;
55 typedef struct Item Item;
56 struct Item {
57 char *text;
58 Item *left, *right;
61 static char text[BUFSIZ] = "";
62 static int barpos = 0;
63 static size_t mw, mh;
64 static size_t lines = 0;
65 static size_t inputw, promptw;
66 static size_t cursor;
67 static char *prompt = NULL;
68 static Item *items = NULL;
69 static Item *matches, *matchend;
70 static Item *prev, *curr, *next, *sel;
71 static struct termios tio_old, tio_new;
72 static int (*fstrncmp)(const char *, const char *, size_t) = strncmp;
74 static void
75 appenditem(Item *item, Item **list, Item **last) {
76 if (!*last)
77 *list = item;
78 else
79 (*last)->right = item;
80 item->left = *last;
81 item->right = NULL;
82 *last = item;
85 static size_t
86 textwn(const char *s, int l) {
87 int b, c; /* bytes and UTF-8 characters */
89 for(b=c=0; s && s[b] && (l<0 || b<l); b++) if((s[b] & 0xc0) != 0x80) c++;
90 return c+4; /* Accomodate for the leading and trailing spaces */
93 static size_t
94 textw(const char *s) {
95 return textwn(s, -1);
98 static void
99 calcoffsets(void) {
100 size_t i, n;
102 if (lines > 0)
103 n = lines;
104 else
105 n = mw - (promptw + inputw + textw("<") + textw(">"));
107 for (i = 0, next = curr; next; next = next->right)
108 if ((i += (lines>0 ? 1 : MIN(textw(next->text), n))) > n)
109 break;
110 for (i = 0, prev = curr; prev && prev->left; prev = prev->left)
111 if ((i += (lines>0 ? 1 : MIN(textw(prev->left->text), n))) > n)
112 break;
115 static void
116 cleanup() {
117 if (barpos == 0) fprintf(stderr, "\n");
118 else fprintf(stderr, "\033[G\033[K");
119 tcsetattr(0, TCSANOW, &tio_old);
122 static void
123 die(const char *s) {
124 tcsetattr(0, TCSANOW, &tio_old);
125 fprintf(stderr, "%s\n", s);
126 exit(2);
129 static void
130 drawtext(const char *t, size_t w, Color col) {
131 const char *prestr, *poststr;
132 size_t i, tw;
133 char *buf;
135 if (w<5) return; /* This is the minimum size needed to write a label: 1 char + 4 padding spaces */
136 tw = w-4; /* This is the text width, without the padding */
137 if (!(buf = calloc(1, tw+1))) die("Can't calloc.");
138 switch (col) {
139 case C_Reverse:
140 prestr="\033[7m";
141 poststr="\033[0m";
142 break;
143 case C_Normal:
144 default:
145 prestr=poststr="";
148 memset(buf, ' ', tw);
149 buf[tw] = '\0';
150 memcpy(buf, t, MIN(strlen(t), tw));
151 if (textw(t) > w) /* Remember textw returns the width WITH padding */
152 for (i = MAX((tw-4), 0); i < tw; i++) buf[i] = '.';
154 fprintf(stderr, "%s %s %s", prestr, buf, poststr);
155 free(buf);
158 static void
159 resetline(void) {
160 if (barpos != 0) fprintf(stderr, "\033[%ldH", (long)(barpos > 0 ? 0 : (mh-lines)));
161 else fprintf(stderr, "\033[%zuF", lines);
164 static void
165 drawmenu(void) {
166 Item *item;
167 size_t rw;
169 /* use default colors */
170 fprintf(stderr, "\033[0m");
172 /* place cursor in first column, clear it */
173 fprintf(stderr, "\033[0G");
174 fprintf(stderr, "\033[K");
176 if (prompt)
177 drawtext(prompt, promptw, C_Reverse);
179 drawtext(text, (lines==0 && matches) ? inputw : mw-promptw, C_Normal);
181 if (lines > 0) {
182 if (barpos != 0) resetline();
183 for (rw = 0, item = curr; item != next; rw++, item = item->right) {
184 fprintf(stderr, "\n");
185 drawtext(item->text, mw, (item == sel) ? C_Reverse : C_Normal);
187 for (; rw < lines; rw++)
188 fprintf(stderr, "\n\033[K");
189 resetline();
190 } else if (matches) {
191 rw = mw-(4+promptw+inputw);
192 if (curr->left)
193 drawtext("<", 5 /*textw("<")*/, C_Normal);
194 for (item = curr; item != next; item = item->right) {
195 drawtext(item->text, MIN(textw(item->text), rw), (item == sel) ? C_Reverse : C_Normal);
196 if ((rw -= textw(item->text)) <= 0) break;
198 if (next) {
199 fprintf(stderr, "\033[%zuG", mw-5);
200 drawtext(">", 5 /*textw(">")*/, C_Normal);
204 fprintf(stderr, "\033[%ldG", (long)(promptw+textwn(text, cursor)-1));
205 fflush(stderr);
208 static char*
209 fstrstr(const char *s, const char *sub) {
210 for (size_t len = strlen(sub); *s; s++)
211 if (!fstrncmp(s, sub, len))
212 return (char*)s;
213 return NULL;
216 static void
217 match(void)
219 static char **tokv = NULL;
220 static int tokn = 0;
222 char buf[sizeof text], *s;
223 int i, tokc = 0;
224 size_t len, textsize;
225 Item *item, *lprefix, *lsubstr, *prefixend, *substrend;
227 strcpy(buf, text);
228 /* separate input text into tokens to be matched individually */
229 for (s = strtok(buf, " "); s; tokv[tokc - 1] = s, s = strtok(NULL, " "))
230 if (++tokc > tokn && !(tokv = realloc(tokv, ++tokn * sizeof *tokv)))
231 die("Can't realloc.");
232 len = tokc ? strlen(tokv[0]) : 0;
234 matches = lprefix = lsubstr = matchend = prefixend = substrend = NULL;
235 textsize = strlen(text) + 1;
236 for (item = items; item && item->text; item++) {
237 for (i = 0; i < tokc; i++)
238 if (!fstrstr(item->text, tokv[i]))
239 break;
240 if (i != tokc) /* not all tokens match */
241 continue;
242 /* exact matches go first, then prefixes, then substrings */
243 if (!tokc || !fstrncmp(text, item->text, textsize))
244 appenditem(item, &matches, &matchend);
245 else if (!fstrncmp(tokv[0], item->text, len))
246 appenditem(item, &lprefix, &prefixend);
247 else
248 appenditem(item, &lsubstr, &substrend);
250 if (lprefix) {
251 if (matches) {
252 matchend->right = lprefix;
253 lprefix->left = matchend;
254 } else
255 matches = lprefix;
256 matchend = prefixend;
258 if (lsubstr) {
259 if (matches) {
260 matchend->right = lsubstr;
261 lsubstr->left = matchend;
262 } else
263 matches = lsubstr;
264 matchend = substrend;
266 curr = sel = matches;
267 calcoffsets();
270 static void
271 insert(const char *str, ssize_t n) {
272 if (strlen(text) + n > sizeof text - 1)
273 return;
274 memmove(&text[cursor + n], &text[cursor], sizeof text - cursor - MAX(n, 0));
275 if (n > 0)
276 memcpy(&text[cursor], str, n);
277 cursor += n;
278 match();
281 static size_t
282 nextrune(int inc) {
283 ssize_t n;
285 for(n = cursor + inc; n + inc >= 0 && (text[n] & 0xc0) == 0x80; n += inc);
286 return n;
289 static void
290 readstdin() {
291 char buf[sizeof text], *p, *maxstr = NULL;
292 size_t i, max = 0, size = 0;
294 for(i = 0; fgets(buf, sizeof buf, stdin); i++) {
295 if (i+1 >= size / sizeof *items)
296 if (!(items = realloc(items, (size += BUFSIZ))))
297 die("Can't realloc.");
298 if ((p = strchr(buf, '\n')))
299 *p = '\0';
300 if (!(items[i].text = strdup(buf)))
301 die("Can't strdup.");
302 if (strlen(items[i].text) > max)
303 max = textw(maxstr = items[i].text);
305 if (items)
306 items[i].text = NULL;
307 inputw = textw(maxstr);
310 static void
311 xread(int fd, void *buf, size_t nbyte) {
312 ssize_t r = read(fd, buf, nbyte);
313 if (r < 0 || (size_t)r != nbyte)
314 die("Can not read.");
317 static void
318 setup(void) {
319 int fd, result = -1;
320 struct winsize ws;
322 /* re-open stdin to read keyboard */
323 if (!freopen("/dev/tty", "r", stdin)) die("Can't reopen tty as stdin.");
324 if (!freopen("/dev/tty", "w", stderr)) die("Can't reopen tty as stderr.");
326 /* ioctl() the tty to get size */
327 fd = open("/dev/tty", O_RDWR);
328 if (fd == -1) {
329 mh = 24;
330 mw = 80;
331 } else {
332 result = ioctl(fd, TIOCGWINSZ, &ws);
333 close(fd);
334 if (result < 0) {
335 mw = 80;
336 mh = 24;
337 } else {
338 mw = ws.ws_col;
339 mh = ws.ws_row;
343 /* change terminal attributes, save old */
344 tcgetattr(0, &tio_old);
345 tio_new = tio_old;
346 tio_new.c_iflag &= ~(BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON);
347 tio_new.c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
348 tio_new.c_cflag &= ~(CSIZE|PARENB);
349 tio_new.c_cflag |= CS8;
350 tio_new.c_cc[VMIN] = 1;
351 tcsetattr(0, TCSANOW, &tio_new);
353 lines = MIN(MAX(lines, 0), mh);
354 promptw = prompt ? textw(prompt) : 0;
355 inputw = MIN(inputw, mw/3);
356 match();
357 if (barpos != 0) resetline();
358 drawmenu();
361 static int
362 run(void) {
363 char buf[32];
364 char c;
366 for (;;) {
367 xread(0, &c, 1);
368 memset(buf, '\0', sizeof buf);
369 buf[0] = c;
370 switch_top:
371 switch(c) {
372 case CONTROL('['):
373 xread(0, &c, 1);
374 esc_switch_top:
375 switch(c) {
376 case CONTROL('['): /* ESC, need to press twice due to console limitations */
377 c = CONTROL('C');
378 goto switch_top;
379 case '[':
380 xread(0, &c, 1);
381 switch(c) {
382 case '1': /* Home */
383 case '7':
384 case 'H':
385 if (c != 'H') xread(0, &c, 1); /* Remove trailing '~' from stdin */
386 c = CONTROL('A');
387 goto switch_top;
388 case '2': /* Insert */
389 xread(0, &c, 1); /* Remove trailing '~' from stdin */
390 c = CONTROL('Y');
391 goto switch_top;
392 case '3': /* Delete */
393 xread(0, &c, 1); /* Remove trailing '~' from stdin */
394 c = CONTROL('D');
395 goto switch_top;
396 case '4': /* End */
397 case '8':
398 case 'F':
399 if (c != 'F') xread(0, &c, 1); /* Remove trailing '~' from stdin */
400 c = CONTROL('E');
401 goto switch_top;
402 case '5': /* PageUp */
403 xread(0, &c, 1); /* Remove trailing '~' from stdin */
404 c = CONTROL('V');
405 goto switch_top;
406 case '6': /* PageDown */
407 xread(0, &c, 1); /* Remove trailing '~' from stdin */
408 c = 'v';
409 goto esc_switch_top;
410 case 'A': /* Up arrow */
411 c = CONTROL('P');
412 goto switch_top;
413 case 'B': /* Down arrow */
414 c = CONTROL('N');
415 goto switch_top;
416 case 'C': /* Right arrow */
417 c = CONTROL('F');
418 goto switch_top;
419 case 'D': /* Left arrow */
420 c = CONTROL('B');
421 goto switch_top;
423 break;
424 case 'b':
425 while (cursor > 0 && text[nextrune(-1)] == ' ')
426 cursor = nextrune(-1);
427 while (cursor > 0 && text[nextrune(-1)] != ' ')
428 cursor = nextrune(-1);
429 break;
430 case 'f':
431 while (text[cursor] != '\0' && text[nextrune(+1)] == ' ')
432 cursor = nextrune(+1);
433 if (text[cursor] != '\0') {
434 do {
435 cursor = nextrune(+1);
436 } while (text[cursor] != '\0' && text[cursor] != ' ');
438 break;
439 case 'd':
440 while (text[cursor] != '\0' && text[nextrune(+1)] == ' ') {
441 cursor = nextrune(+1);
442 insert(NULL, nextrune(-1) - cursor);
444 if (text[cursor] != '\0') {
445 do {
446 cursor = nextrune(+1);
447 insert(NULL, nextrune(-1) - cursor);
448 } while (text[cursor] != '\0' && text[cursor] != ' ');
450 break;
451 case 'v':
452 if (!next)
453 break;
454 sel = curr = next;
455 calcoffsets();
456 break;
457 default:
458 break;
460 break;
461 case CONTROL('C'):
462 return 1;
463 case CONTROL('M'): /* Return */
464 case CONTROL('J'):
465 if (sel) strncpy(text, sel->text, sizeof(text)-1); /* Complete the input first, when hitting return */
466 cursor = strlen(text);
467 match();
468 drawmenu();
469 /* fallthrough */
470 case CONTROL(']'):
471 case CONTROL('\\'): /* These are usually close enough to RET to replace Shift+RET, again due to console limitations */
472 puts(text);
473 return 0;
474 case CONTROL('A'):
475 if (sel == matches) {
476 cursor = 0;
477 break;
479 sel = curr = matches;
480 calcoffsets();
481 break;
482 case CONTROL('E'):
483 if (text[cursor] != '\0') {
484 cursor = strlen(text);
485 break;
487 if (next) {
488 curr = matchend;
489 calcoffsets();
490 curr = prev;
491 calcoffsets();
492 while(next && (curr = curr->right))
493 calcoffsets();
495 sel = matchend;
496 break;
497 case CONTROL('B'):
498 if (cursor > 0 && (!sel || !sel->left || lines > 0)) {
499 cursor = nextrune(-1);
500 break;
502 /* fallthrough */
503 case CONTROL('P'):
504 if (sel && sel->left && (sel = sel->left)->right == curr) {
505 curr = prev;
506 calcoffsets();
508 break;
509 case CONTROL('F'):
510 if (text[cursor] != '\0') {
511 cursor = nextrune(+1);
512 break;
514 /* fallthrough */
515 case CONTROL('N'):
516 if (sel && sel->right && (sel = sel->right) == next) {
517 curr = next;
518 calcoffsets();
520 break;
521 case CONTROL('D'):
522 if (text[cursor] == '\0')
523 break;
524 cursor = nextrune(+1);
525 /* fallthrough */
526 case CONTROL('H'):
527 case CONTROL('?'): /* Backspace */
528 if (cursor == 0)
529 break;
530 insert(NULL, nextrune(-1) - cursor);
531 break;
532 case CONTROL('I'): /* TAB */
533 if (!sel)
534 break;
535 strncpy(text, sel->text, sizeof text);
536 cursor = strlen(text);
537 match();
538 break;
539 case CONTROL('K'):
540 text[cursor] = '\0';
541 match();
542 break;
543 case CONTROL('U'):
544 insert(NULL, 0 - cursor);
545 break;
546 case CONTROL('W'):
547 while (cursor > 0 && text[nextrune(-1)] == ' ')
548 insert(NULL, nextrune(-1) - cursor);
549 while (cursor > 0 && text[nextrune(-1)] != ' ')
550 insert(NULL, nextrune(-1) - cursor);
551 break;
552 case CONTROL('V'):
553 if (!prev)
554 break;
555 sel = curr = prev;
556 calcoffsets();
557 break;
558 default:
559 if (!iscntrl(*buf))
560 insert(buf, strlen(buf));
561 break;
563 drawmenu();
567 static void
568 usage(void) {
569 fputs("usage: vis-menu [-b|-t] [-i] [-l lines] [-p prompt] [initial selection]\n", stderr);
570 exit(2);
574 main(int argc, char **argv) {
575 for (int i = 1; i < argc; i++) {
576 if (!strcmp(argv[i], "-v")) {
577 puts("vis-menu " VERSION);
578 exit(0);
579 } else if (!strcmp(argv[i], "-i")) {
580 fstrncmp = strncasecmp;
581 } else if (!strcmp(argv[i], "-t")) {
582 barpos = +1;
583 } else if (!strcmp(argv[i], "-b")) {
584 barpos = -1;
585 } else if (argv[i][0] != '-') {
586 strncpy(text, argv[i], sizeof(text)-1);
587 cursor = strlen(text);
588 } else if (i + 1 == argc) {
589 usage();
590 } else if (!strcmp(argv[i], "-p")) {
591 prompt = argv[++i];
592 if (prompt && !prompt[0])
593 prompt = NULL;
594 } else if (!strcmp(argv[i], "-l")) {
595 errno = 0;
596 lines = strtoul(argv[++i], NULL, 10);
597 if (errno)
598 usage();
599 } else {
600 usage();
604 readstdin();
605 setup();
606 int status = run();
607 cleanup();
608 return status;