vis
a vi-like editor based on Plan 9's structural regular expressions
git clone https://9o.is/git/vis.git
vis-menu.c
(15882B)
1 /*
2 * MIT/X Consortium License
3 *
4 * © 2011 Rafael Garcia Gallego <rafael.garcia.gallego@gmail.com>
5 *
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>
14 *
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:
21 *
22 * The above copyright notice and this permission notice shall be included in
23 * all copies or substantial portions of the Software.
24 *
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.
32 */
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>
45
46 #define CONTROL(ch) (ch ^ 0x40)
47 #define MIN(a,b) ((a) < (b) ? (a) : (b))
48 #define MAX(a,b) ((a) > (b) ? (a) : (b))
49
50 typedef enum {
51 C_Normal,
52 C_Reverse
53 } Color;
54
55 typedef struct Item Item;
56 struct Item {
57 char *text;
58 Item *left, *right;
59 };
60
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;
73
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;
83 }
84
85 static size_t
86 textwn(const char *s, int l) {
87 int c;
88
89 for (c=0; s && s[c] && (l<0 || c<l); c++);
90 return c+4; /* Accomodate for the leading and trailing spaces */
91 }
92
93 /*
94 * textvalidn returns the highest amount of bytes <= l of string s that
95 * only contains valid Unicode points. This is used to make sure we don't
96 * cut off any valid UTF-8-encoded unicode point in case there is not
97 * enough space to render the whole text string.
98 */
99 static ssize_t
100 textvalidn(const char *s, int l) {
101 int c, utfcharbytes; /* byte count and UTF-8 codepoint length */
102
103 for (c=0; s && s[c] && (l<0 || c<l); ) {
104 utfcharbytes = 0;
105 if ((s[c] & 0x80) == 0) {
106 utfcharbytes = 1;
107 } else if ((s[c] & 0xf0) == 0xf0) {
108 utfcharbytes = 4;
109 } else if ((s[c] & 0xf0) == 0xe0) {
110 utfcharbytes = 3;
111 } else if ((s[c] & 0xe0) == 0xc0) {
112 utfcharbytes = 2;
113 } else {
114 return -1;
115 }
116
117 if ((l>0 && c + utfcharbytes >= l)) {
118 break;
119 }
120
121 c += utfcharbytes;
122 }
123
124 return c;
125 }
126
127 static size_t
128 textw(const char *s) {
129 return textwn(s, -1);
130 }
131
132 static void
133 calcoffsets(void) {
134 size_t i, n;
135
136 if (lines > 0)
137 n = lines;
138 else
139 n = mw - (promptw + inputw + textw("<") + textw(">"));
140
141 for (i = 0, next = curr; next; next = next->right)
142 if ((i += (lines>0 ? 1 : MIN(textw(next->text), n))) > n)
143 break;
144 for (i = 0, prev = curr; prev && prev->left; prev = prev->left)
145 if ((i += (lines>0 ? 1 : MIN(textw(prev->left->text), n))) > n)
146 break;
147 }
148
149 static void
150 cleanup(void) {
151 if (barpos == 0) fprintf(stderr, "\n");
152 else fprintf(stderr, "\033[G\033[K");
153 tcsetattr(0, TCSANOW, &tio_old);
154 }
155
156 static void
157 die(const char *s) {
158 tcsetattr(0, TCSANOW, &tio_old);
159 fprintf(stderr, "%s\n", s);
160 exit(2);
161 }
162
163 static void
164 drawtext(const char *t, size_t w, Color col) {
165 const char *prestr, *poststr;
166 size_t i, tw;
167 char *buf;
168
169 if (w<5) return; /* This is the minimum size needed to write a label: 1 char + 4 padding spaces */
170 tw = w-4; /* This is the text width, without the padding */
171 if (!(buf = calloc(1, tw+1))) die("Can't calloc.");
172 switch (col) {
173 case C_Reverse:
174 prestr="\033[7m";
175 poststr="\033[0m";
176 break;
177 case C_Normal:
178 default:
179 prestr=poststr="";
180 }
181
182 memset(buf, ' ', tw);
183 buf[tw] = '\0';
184 memcpy(buf, t, MIN(strlen(t), tw));
185 if (textw(t) > w) /* Remember textw returns the width WITH padding */
186 for (i = MAX(textvalidn(t, w-4), 0); i < tw; i++) buf[i] = '.';
187
188 fprintf(stderr, "%s %s %s", prestr, buf, poststr);
189 free(buf);
190 }
191
192 static void
193 resetline(void) {
194 if (barpos != 0) fprintf(stderr, "\033[%ldH", (long)(barpos > 0 ? 0 : (mh-lines)));
195 else fprintf(stderr, "\033[%zuF", lines);
196 }
197
198 static void
199 drawmenu(void) {
200 Item *item;
201 size_t rw;
202
203 /* use default colors */
204 fprintf(stderr, "\033[0m");
205
206 /* place cursor in first column, clear it */
207 fprintf(stderr, "\033[0G");
208 fprintf(stderr, "\033[K");
209
210 if (prompt)
211 drawtext(prompt, promptw, C_Reverse);
212
213 drawtext(text, (lines==0 && matches) ? inputw : mw-promptw, C_Normal);
214
215 if (lines > 0) {
216 if (barpos != 0) resetline();
217 for (rw = 0, item = curr; item != next; rw++, item = item->right) {
218 fprintf(stderr, "\n");
219 drawtext(item->text, mw, (item == sel) ? C_Reverse : C_Normal);
220 }
221 for (; rw < lines; rw++)
222 fprintf(stderr, "\n\033[K");
223 resetline();
224 } else if (matches) {
225 rw = mw-(4+promptw+inputw);
226 if (curr->left)
227 drawtext("<", 5 /*textw("<")*/, C_Normal);
228 for (item = curr; item != next; item = item->right) {
229 drawtext(item->text, MIN(textw(item->text), rw), (item == sel) ? C_Reverse : C_Normal);
230 if ((rw -= textw(item->text)) <= 0) break;
231 }
232 if (next) {
233 fprintf(stderr, "\033[%zuG", mw-5);
234 drawtext(">", 5 /*textw(">")*/, C_Normal);
235 }
236
237 }
238 fprintf(stderr, "\033[%ldG", (long)(promptw+textwn(text, cursor)-1));
239 fflush(stderr);
240 }
241
242 static char*
243 fstrstr(const char *s, const char *sub) {
244 for (size_t len = strlen(sub); *s; s++)
245 if (!fstrncmp(s, sub, len))
246 return (char*)s;
247 return NULL;
248 }
249
250 static void
251 match(void)
252 {
253 static char **tokv = NULL;
254 static int tokn = 0;
255
256 char buf[sizeof text], *s;
257 int i, tokc = 0;
258 size_t len, textsize;
259 Item *item, *lprefix, *lsubstr, *prefixend, *substrend;
260
261 strcpy(buf, text);
262 /* separate input text into tokens to be matched individually */
263 for (s = strtok(buf, " "); s; tokv[tokc - 1] = s, s = strtok(NULL, " "))
264 if (++tokc > tokn && !(tokv = realloc(tokv, ++tokn * sizeof *tokv)))
265 die("Can't realloc.");
266 len = tokc ? strlen(tokv[0]) : 0;
267
268 matches = lprefix = lsubstr = matchend = prefixend = substrend = NULL;
269 textsize = strlen(text) + 1;
270 for (item = items; item && item->text; item++) {
271 for (i = 0; i < tokc; i++)
272 if (!fstrstr(item->text, tokv[i]))
273 break;
274 if (i != tokc) /* not all tokens match */
275 continue;
276 /* exact matches go first, then prefixes, then substrings */
277 if (!tokc || !fstrncmp(text, item->text, textsize))
278 appenditem(item, &matches, &matchend);
279 else if (!fstrncmp(tokv[0], item->text, len))
280 appenditem(item, &lprefix, &prefixend);
281 else
282 appenditem(item, &lsubstr, &substrend);
283 }
284 if (lprefix) {
285 if (matches) {
286 matchend->right = lprefix;
287 lprefix->left = matchend;
288 } else
289 matches = lprefix;
290 matchend = prefixend;
291 }
292 if (lsubstr) {
293 if (matches) {
294 matchend->right = lsubstr;
295 lsubstr->left = matchend;
296 } else
297 matches = lsubstr;
298 matchend = substrend;
299 }
300 curr = sel = matches;
301 calcoffsets();
302 }
303
304 static void
305 insert(const char *str, ssize_t n) {
306 if (strlen(text) + n > sizeof text - 1)
307 return;
308 memmove(&text[cursor + n], &text[cursor], sizeof text - cursor - MAX(n, 0));
309 if (n > 0)
310 memcpy(&text[cursor], str, n);
311 cursor += n;
312 match();
313 }
314
315 static size_t
316 nextrune(int inc) {
317 ssize_t n;
318
319 for(n = cursor + inc; n + inc >= 0 && (text[n] & 0xc0) == 0x80; n += inc);
320 return n;
321 }
322
323 static void
324 readstdin(void) {
325 char buf[sizeof text], *p, *maxstr = NULL;
326 size_t i, max = 0, size = 0;
327
328 for(i = 0; fgets(buf, sizeof buf, stdin); i++) {
329 if (i+1 >= size / sizeof *items)
330 if (!(items = realloc(items, (size += BUFSIZ))))
331 die("Can't realloc.");
332 if ((p = strchr(buf, '\n')))
333 *p = '\0';
334 if (!(items[i].text = strdup(buf)))
335 die("Can't strdup.");
336 if (strlen(items[i].text) > max)
337 max = textw(maxstr = items[i].text);
338 }
339 if (items)
340 items[i].text = NULL;
341 inputw = textw(maxstr);
342 }
343
344 static void
345 xread(int fd, void *buf, size_t nbyte) {
346 ssize_t r = read(fd, buf, nbyte);
347 if (r < 0 || (size_t)r != nbyte)
348 die("Can not read.");
349 }
350
351 static void
352 setup(void) {
353 int fd, result = -1;
354 struct winsize ws;
355
356 /* re-open stdin to read keyboard */
357 if (!freopen("/dev/tty", "r", stdin)) die("Can't reopen tty as stdin.");
358 if (!freopen("/dev/tty", "w", stderr)) die("Can't reopen tty as stderr.");
359
360 /* ioctl() the tty to get size */
361 fd = open("/dev/tty", O_RDWR);
362 if (fd == -1) {
363 mh = 24;
364 mw = 80;
365 } else {
366 result = ioctl(fd, TIOCGWINSZ, &ws);
367 close(fd);
368 if (result < 0) {
369 mw = 80;
370 mh = 24;
371 } else {
372 mw = ws.ws_col;
373 mh = ws.ws_row;
374 }
375 }
376
377 /* change terminal attributes, save old */
378 tcgetattr(0, &tio_old);
379 tio_new = tio_old;
380 tio_new.c_iflag &= ~(BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON);
381 tio_new.c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
382 tio_new.c_cflag &= ~(CSIZE|PARENB);
383 tio_new.c_cflag |= CS8;
384 tio_new.c_cc[VMIN] = 1;
385 tcsetattr(0, TCSANOW, &tio_new);
386
387 lines = MIN(MAX(lines, 0), mh);
388 promptw = prompt ? textw(prompt) : 0;
389 inputw = MIN(inputw, mw/3);
390 match();
391 if (barpos != 0) resetline();
392 drawmenu();
393 }
394
395 static int
396 run(void) {
397 char buf[32];
398 char c;
399
400 for (;;) {
401 xread(0, &c, 1);
402 memset(buf, '\0', sizeof buf);
403 buf[0] = c;
404 switch_top:
405 switch(c) {
406 case CONTROL('['):
407 xread(0, &c, 1);
408 esc_switch_top:
409 switch(c) {
410 case CONTROL('['): /* ESC, need to press twice due to console limitations */
411 c = CONTROL('C');
412 goto switch_top;
413 case '[':
414 xread(0, &c, 1);
415 switch(c) {
416 case '1': /* Home */
417 case '7':
418 case 'H':
419 if (c != 'H') xread(0, &c, 1); /* Remove trailing '~' from stdin */
420 c = CONTROL('A');
421 goto switch_top;
422 case '2': /* Insert */
423 xread(0, &c, 1); /* Remove trailing '~' from stdin */
424 c = CONTROL('Y');
425 goto switch_top;
426 case '3': /* Delete */
427 xread(0, &c, 1); /* Remove trailing '~' from stdin */
428 c = CONTROL('D');
429 goto switch_top;
430 case '4': /* End */
431 case '8':
432 case 'F':
433 if (c != 'F') xread(0, &c, 1); /* Remove trailing '~' from stdin */
434 c = CONTROL('E');
435 goto switch_top;
436 case '5': /* PageUp */
437 xread(0, &c, 1); /* Remove trailing '~' from stdin */
438 c = CONTROL('V');
439 goto switch_top;
440 case '6': /* PageDown */
441 xread(0, &c, 1); /* Remove trailing '~' from stdin */
442 c = 'v';
443 goto esc_switch_top;
444 case 'A': /* Up arrow */
445 c = CONTROL('P');
446 goto switch_top;
447 case 'B': /* Down arrow */
448 c = CONTROL('N');
449 goto switch_top;
450 case 'C': /* Right arrow */
451 c = CONTROL('F');
452 goto switch_top;
453 case 'D': /* Left arrow */
454 c = CONTROL('B');
455 goto switch_top;
456 }
457 break;
458 case 'b':
459 while (cursor > 0 && text[nextrune(-1)] == ' ')
460 cursor = nextrune(-1);
461 while (cursor > 0 && text[nextrune(-1)] != ' ')
462 cursor = nextrune(-1);
463 break;
464 case 'f':
465 while (text[cursor] != '\0' && text[nextrune(+1)] == ' ')
466 cursor = nextrune(+1);
467 if (text[cursor] != '\0') {
468 do {
469 cursor = nextrune(+1);
470 } while (text[cursor] != '\0' && text[cursor] != ' ');
471 }
472 break;
473 case 'd':
474 while (text[cursor] != '\0' && text[nextrune(+1)] == ' ') {
475 cursor = nextrune(+1);
476 insert(NULL, nextrune(-1) - cursor);
477 }
478 if (text[cursor] != '\0') {
479 do {
480 cursor = nextrune(+1);
481 insert(NULL, nextrune(-1) - cursor);
482 } while (text[cursor] != '\0' && text[cursor] != ' ');
483 }
484 break;
485 case 'v':
486 if (!next)
487 break;
488 sel = curr = next;
489 calcoffsets();
490 break;
491 default:
492 break;
493 }
494 break;
495 case CONTROL('C'):
496 return 1;
497 case CONTROL('M'): /* Return */
498 case CONTROL('J'):
499 if (sel) strncpy(text, sel->text, sizeof(text)-1); /* Complete the input first, when hitting return */
500 cursor = strlen(text);
501 match();
502 drawmenu();
503 /* fallthrough */
504 case CONTROL(']'):
505 case CONTROL('\\'): /* These are usually close enough to RET to replace Shift+RET, again due to console limitations */
506 puts(text);
507 return 0;
508 case CONTROL('A'):
509 if (sel == matches) {
510 cursor = 0;
511 break;
512 }
513 sel = curr = matches;
514 calcoffsets();
515 break;
516 case CONTROL('E'):
517 if (text[cursor] != '\0') {
518 cursor = strlen(text);
519 break;
520 }
521 if (next) {
522 curr = matchend;
523 calcoffsets();
524 curr = prev;
525 calcoffsets();
526 while(next && (curr = curr->right))
527 calcoffsets();
528 }
529 sel = matchend;
530 break;
531 case CONTROL('B'):
532 if (cursor > 0 && (!sel || !sel->left || lines > 0)) {
533 cursor = nextrune(-1);
534 break;
535 }
536 /* fallthrough */
537 case CONTROL('P'):
538 if (sel && sel->left && (sel = sel->left)->right == curr) {
539 curr = prev;
540 calcoffsets();
541 }
542 break;
543 case CONTROL('F'):
544 if (text[cursor] != '\0') {
545 cursor = nextrune(+1);
546 break;
547 }
548 /* fallthrough */
549 case CONTROL('N'):
550 if (sel && sel->right && (sel = sel->right) == next) {
551 curr = next;
552 calcoffsets();
553 }
554 break;
555 case CONTROL('D'):
556 if (text[cursor] == '\0')
557 break;
558 cursor = nextrune(+1);
559 /* fallthrough */
560 case CONTROL('H'):
561 case CONTROL('?'): /* Backspace */
562 if (cursor == 0)
563 break;
564 insert(NULL, nextrune(-1) - cursor);
565 break;
566 case CONTROL('I'): /* TAB */
567 if (!sel)
568 break;
569 strncpy(text, sel->text, sizeof(text)-1);
570 cursor = strlen(text);
571 match();
572 break;
573 case CONTROL('K'):
574 text[cursor] = '\0';
575 match();
576 break;
577 case CONTROL('U'):
578 insert(NULL, 0 - cursor);
579 break;
580 case CONTROL('W'):
581 while (cursor > 0 && text[nextrune(-1)] == ' ')
582 insert(NULL, nextrune(-1) - cursor);
583 while (cursor > 0 && text[nextrune(-1)] != ' ')
584 insert(NULL, nextrune(-1) - cursor);
585 break;
586 case CONTROL('V'):
587 if (!prev)
588 break;
589 sel = curr = prev;
590 calcoffsets();
591 break;
592 default:
593 if (!iscntrl(*buf))
594 insert(buf, strlen(buf));
595 break;
596 }
597 drawmenu();
598 }
599 }
600
601 static void
602 usage(void) {
603 fputs("usage: vis-menu [-b|-t] [-i] [-l lines] [-p prompt] [initial selection]\n", stderr);
604 exit(2);
605 }
606
607 int
608 main(int argc, char **argv) {
609 for (int i = 1; i < argc; i++) {
610 if (!strcmp(argv[i], "-v")) {
611 puts("vis-menu " VERSION);
612 exit(0);
613 } else if (!strcmp(argv[i], "-i")) {
614 fstrncmp = strncasecmp;
615 } else if (!strcmp(argv[i], "-t")) {
616 barpos = +1;
617 } else if (!strcmp(argv[i], "-b")) {
618 barpos = -1;
619 } else if (argv[i][0] != '-') {
620 strncpy(text, argv[i], sizeof(text)-1);
621 cursor = strlen(text);
622 } else if (i + 1 == argc) {
623 usage();
624 } else if (!strcmp(argv[i], "-p")) {
625 prompt = argv[++i];
626 if (prompt && !prompt[0])
627 prompt = NULL;
628 } else if (!strcmp(argv[i], "-l")) {
629 errno = 0;
630 lines = strtoul(argv[++i], NULL, 10);
631 if (errno)
632 usage();
633 } else {
634 usage();
635 }
636 }
637
638 readstdin();
639 setup();
640 int status = run();
641 cleanup();
642 return status;
643 }