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 }