tmenu

Terminal menu for selecting items from stdin
git clone https://git.sinitax.com/sinitax/tmenu
Log | Files | Refs | README | LICENSE | sfeed.txt

tmenu.c (15636B)


      1#include <sys/ioctl.h>
      2#include <signal.h>
      3#include <unistd.h>
      4#include <termios.h>
      5#include <fcntl.h>
      6#include <stdbool.h>
      7#include <stdarg.h>
      8#include <string.h>
      9#include <stdio.h>
     10#include <stdlib.h>
     11
     12#define ARRLEN(x) (sizeof(x)/sizeof((x)[0]))
     13#define MAX(a, b) ((a) > (b) ? (a) : (b))
     14#define MIN(a, b) ((a) < (b) ? (a) : (b))
     15#define EPRINTF(...) fprintf(stderr, __VA_ARGS__)
     16
     17#define KEY_CTRL(c) (((int) (c)) & 0b11111)
     18
     19#define CSI_CLEAR_LINE   "\x1b[K\r"
     20#define CSI_CUR_HIDE     "\x1b[?25l"
     21#define CSI_CUR_SHOW     "\x1b[?25h"
     22#define CSI_CUR_UP       "\x1b[A"
     23#define CSI_CUR_DOWN     "\x1b[B"
     24#define CSI_CUR_RIGHT    "\x1b[C"
     25#define CSI_CUR_LEFT     "\x1b[D"
     26#define CSI_STYLE_BOLD   "\x1b[1m"
     27#define CSI_STYLE_RESET  "\x1b[0m"
     28#define CSI_CLEAR_SCREEN "\x1b[2J"
     29#define CSI_STYLE_ULINE  "\x1b[4m"
     30#define CSI_STYLE_NULINE "\x1b[24m"
     31#define CSI_CUR_GOTO     "\x1b[%i%iH"
     32
     33enum {
     34	BWD = -1,
     35	FWD = 1,
     36};
     37
     38enum {
     39	MODE_BROWSE,
     40	MODE_SEARCH,
     41};
     42
     43enum {
     44	SEARCH_SUBSTR,
     45	SEARCH_FUZZY,
     46};
     47
     48enum {
     49	CASE_SENSITIVE,
     50	CASE_INSENSITIVE,
     51};
     52
     53enum {
     54	KEY_NONE = 0,
     55	KEY_DEL = 0x7f,
     56	KEY_UP = 0x100,
     57	KEY_DOWN,
     58	KEY_LEFT,
     59	KEY_RIGHT,
     60	KEY_PGUP,
     61	KEY_PGDN,
     62};
     63
     64struct mode {
     65	void (*prompt)(void);
     66	void (*cleanup)(void);
     67	bool (*handlekey)(int c);
     68};
     69
     70struct searchmode {
     71	char c;
     72	ssize_t (*match)(ssize_t, int, size_t, ssize_t, bool, bool);
     73};
     74
     75static void browse_prompt(void);
     76static bool browse_handlekey(int c);
     77static void browse_cleanup(void);
     78
     79static void search_prompt(void);
     80static bool search_handlekey(int c);
     81static void search_cleanup(void);
     82
     83static ssize_t search_match(ssize_t start, int dir,
     84	size_t cnt, ssize_t fallback, bool new, bool closest);
     85static ssize_t search_match_substr(ssize_t start, int dir,
     86	size_t cnt, ssize_t fallback, bool new, bool closest);
     87static ssize_t search_match_fuzzy(ssize_t start, int dir,
     88	size_t cnt, ssize_t fallback, bool new, bool closest);
     89
     90static const struct searchmode searchmodes[] = {
     91	[SEARCH_SUBSTR] = {
     92		.c = 'S',
     93		.match = search_match_substr,
     94	},
     95	[SEARCH_FUZZY] = {
     96		.c = 'F',
     97		.match = search_match_fuzzy,
     98	}
     99};
    100
    101static const struct mode modes[] = {
    102	[MODE_BROWSE] = {
    103		.prompt = browse_prompt,
    104		.handlekey = browse_handlekey,
    105		.cleanup = browse_cleanup
    106	},
    107	[MODE_SEARCH] = {
    108		.prompt = search_prompt,
    109		.handlekey = search_handlekey,
    110		.cleanup = search_cleanup
    111	}
    112};
    113
    114static bool init = false;
    115
    116static char *input = NULL;
    117static size_t input_len = 0;
    118static size_t input_cap = 0;
    119
    120static size_t *delims = NULL;
    121static size_t delims_cnt = 0;
    122static size_t delims_cap = 0;
    123
    124static ssize_t selected = -1;
    125static const char *entry = NULL;
    126static size_t entry_max = 0;
    127static size_t entry_off = 0;
    128
    129static char searchbuf[1024];
    130static size_t searchlen = 0;
    131
    132static int mode = MODE_BROWSE;
    133static int searchcase = CASE_SENSITIVE;
    134static int searchmode = SEARCH_SUBSTR;
    135
    136static size_t fwdctx = 1;
    137static size_t bwdctx = 1;
    138static size_t termw = 80;
    139
    140static bool multiout = false;
    141static bool verbose = false;
    142static bool show_prompt = true;
    143static bool top_prompt = false;
    144
    145static char delim = '\n';
    146
    147static void
    148die(const char *fmt, ...)
    149{
    150	va_list ap;
    151
    152	va_start(ap, fmt);
    153	fputs("tmenu: ", stderr);
    154	vfprintf(stderr, fmt, ap);
    155	if (*fmt && fmt[strlen(fmt) - 1] == ':') {
    156		fputc(' ', stderr);
    157		perror(NULL);
    158	} else {
    159		fputc('\n', stderr);
    160	}
    161	va_end(ap);
    162
    163	exit(1);
    164}
    165
    166static void
    167prompt(void)
    168{
    169	struct winsize ws = { 0 };
    170
    171	if (ioctl(2, TIOCGWINSZ, &ws) != -1)
    172		termw = ws.ws_col;
    173
    174	modes[mode].prompt();
    175}
    176
    177static void
    178sigwinch(int sig)
    179{
    180	if (init) prompt();
    181	signal(SIGWINCH, sigwinch);
    182}
    183
    184static void *
    185addcap(void *alloc, size_t dsize, size_t min, size_t *cap)
    186{
    187	if (min > *cap) {
    188		*cap = *cap * 2;
    189		if (*cap < min) *cap = min;
    190		alloc = realloc(alloc, dsize * *cap);
    191		if (!alloc) die("realloc:");
    192	}
    193
    194	return alloc;
    195}
    196
    197static inline char
    198lower(char c)
    199{
    200	if (c >= 'A' && c <= 'Z')
    201		c += 'a' - 'A';
    202	return c;
    203}
    204
    205static size_t
    206entry_len(size_t index)
    207{
    208	return delims[index] - (index > 0 ? delims[index-1] + 1 : 0);
    209}
    210
    211static inline char *
    212get_entry(size_t index)
    213{
    214	return input + (index > 0 ? delims[index-1] + 1 : 0);
    215}
    216
    217static int
    218readkey(FILE *f)
    219{
    220	int c;
    221
    222	c = fgetc(f);
    223	if (c != '\x1b')
    224		return c;
    225
    226	if (fgetc(f) != '[')
    227		return KEY_NONE;
    228
    229	switch (fgetc(f)) {
    230	case 'A':
    231		return KEY_UP;
    232	case 'B':
    233		return KEY_DOWN;
    234	case 'C':
    235		return KEY_RIGHT;
    236	case 'D':
    237		return KEY_LEFT;
    238	case '5':
    239		return fgetc(f) == '~' ? KEY_PGUP : KEY_NONE;
    240	case '6':
    241		return fgetc(f) == '~' ? KEY_PGDN : KEY_NONE;
    242	}
    243
    244	return KEY_NONE;
    245}
    246
    247static int
    248search_eq(const char *a, const char *b, size_t size)
    249{
    250	size_t i;
    251
    252	for (i = 0; i < size; i++) {
    253		if (searchcase == CASE_SENSITIVE) {
    254			if (a[i] != b[i]) return false;
    255		} else {
    256			if (lower(a[i]) != lower(b[i]))
    257				return false;
    258		}
    259	}
    260
    261	return true;
    262}
    263
    264static const char*
    265search_find(const char *a, char c, size_t size)
    266{
    267	size_t i;
    268
    269	for (i = 0; i < size; i++) {
    270		if (searchcase == CASE_SENSITIVE) {
    271			if (a[i] == c) return a + i;
    272		} else if (searchcase == CASE_INSENSITIVE) {
    273			if (lower(a[i]) == lower(c))
    274				return a + i;
    275		}
    276	}
    277
    278	return NULL;
    279}
    280
    281static void
    282browse_prompt(void)
    283{
    284	size_t linew, entlen;
    285	ssize_t i;
    286
    287	if (selected < 0) selected = 0;
    288
    289	if (show_prompt && top_prompt)
    290		EPRINTF(CSI_STYLE_BOLD "[B] " CSI_STYLE_RESET "\n");
    291
    292	i = (ssize_t) selected - (ssize_t) bwdctx;
    293	for (; i <= (ssize_t) selected + (ssize_t) fwdctx; i++) {
    294		EPRINTF(CSI_CLEAR_LINE);
    295
    296		if (i == selected)
    297			EPRINTF(CSI_STYLE_BOLD);
    298
    299		if (show_prompt && !top_prompt) {
    300			linew = termw - 4;
    301			if (i == selected) {
    302				EPRINTF("[B] ");
    303			} else {
    304				EPRINTF("    ");
    305			}
    306		} else {
    307			linew = termw;
    308		}
    309
    310		if (selected >= 0 && i >= 0 && i < delims_cnt) {
    311			entry = get_entry((size_t) i);
    312			entlen = entry_len((size_t) i);
    313			if (entlen > linew) {
    314				EPRINTF(" ..%.*s\n", (int) (linew - 3),
    315					entry + entlen - (linew - 3));
    316			} else {
    317				EPRINTF("%.*s\n", (int) linew, entry);
    318			}
    319		} else {
    320			EPRINTF("\n");
    321		}
    322
    323		if (i == selected)
    324			EPRINTF(CSI_STYLE_RESET);
    325	}
    326
    327	for (i = 0; i < bwdctx + fwdctx + 1 + show_prompt * top_prompt; i++)
    328		EPRINTF(CSI_CUR_UP);
    329}
    330
    331static bool
    332browse_handlekey(int c)
    333{
    334	size_t cnt;
    335
    336	switch (c) {
    337	case 'g':
    338		selected = 0;
    339		break;
    340	case 'G':
    341		selected = (ssize_t) delims_cnt - 1;
    342		break;
    343	case 'q':
    344		return true;
    345	case KEY_PGUP:
    346		cnt = fwdctx + bwdctx + 1;
    347		if (selected > cnt)
    348			selected -= (ssize_t) cnt;
    349		else
    350			selected = 0;
    351		break;
    352	case KEY_PGDN:
    353		cnt = fwdctx + bwdctx + 1;
    354		if (selected < delims_cnt - cnt)
    355			selected += (ssize_t) cnt;
    356		else
    357			selected = (ssize_t) delims_cnt - 1;
    358		break;
    359	case KEY_UP:
    360		if (selected != 0)
    361			selected--;
    362		break;
    363	case KEY_DOWN:
    364		if (selected != delims_cnt - 1)
    365			selected++;
    366		break;
    367	}
    368
    369	return false;
    370}
    371
    372static void
    373browse_cleanup(void)
    374{
    375	size_t i;
    376
    377	for (i = 0; i < bwdctx + 1 + fwdctx; i++)
    378		EPRINTF(CSI_CLEAR_LINE "\n");
    379	for (i = 0; i < bwdctx + 1 + fwdctx; i++)
    380		EPRINTF(CSI_CUR_UP);
    381}
    382
    383static void
    384search_prompt(void)
    385{
    386	size_t linew, off, entlen;
    387	ssize_t i, index;
    388
    389	if (selected < 0) selected = 0;
    390
    391	index = search_match(selected, FWD, 1, -1, false, true);
    392	if (index != -1) {
    393		selected = index;
    394	} else {
    395		selected = search_match(selected, BWD, 1, -1, true, true);
    396	}
    397
    398	if (show_prompt && top_prompt) {
    399		EPRINTF(CSI_STYLE_BOLD "[%c] " CSI_STYLE_ULINE "%.*s"
    400			CSI_STYLE_NULINE "%.*s\n" CSI_STYLE_RESET,
    401			(searchcase == CASE_SENSITIVE ?
    402			searchmodes[searchmode].c
    403			: lower(searchmodes[searchmode].c)),
    404			(int) searchlen, searchbuf, (int) termw, " ");
    405	}
    406
    407	for (i = -(ssize_t) bwdctx; i <= (ssize_t) fwdctx; i++) {
    408		if (selected >= 0) {
    409			if (i < 0) {
    410				index = search_match(selected,
    411					BWD, (size_t) -i, -1, true, false);
    412			} else if (i == 0) {
    413				index = selected;
    414			} else if (i > 0) {
    415				index = search_match(selected,
    416					FWD, (size_t) i, -1, true, false);
    417			}
    418		} else {
    419			index = -1;
    420		}
    421
    422		EPRINTF(CSI_CLEAR_LINE);
    423
    424		if (i == 0) EPRINTF(CSI_STYLE_BOLD);
    425
    426		if (show_prompt && !top_prompt) {
    427			if (i == 0) {
    428				EPRINTF("[%c] " CSI_STYLE_ULINE "%.*s"
    429					CSI_STYLE_NULINE " ",
    430					(searchcase == CASE_SENSITIVE ?
    431					searchmodes[searchmode].c
    432					: lower(searchmodes[searchmode].c)),
    433					(int) searchlen, searchbuf);
    434			} else {
    435				EPRINTF("%*.s", (int) (4 + searchlen + 1), " ");
    436			}
    437			linew = (size_t) MAX(0, (ssize_t) termw
    438				- (ssize_t) searchlen - 5);
    439		} else {
    440			linew = termw;
    441		}
    442
    443		if (index < 0) {
    444			EPRINTF("\n");
    445		} else {
    446			entlen = entry_len((size_t) index);
    447			entry = get_entry((size_t) index);
    448			off = entlen >= linew ? entlen - linew : 0;
    449			off = MIN(entry_off, off);
    450			EPRINTF("%.*s\n", (int) linew, entry + off);
    451		}
    452
    453		if (i == 0) EPRINTF(CSI_STYLE_RESET);
    454	}
    455
    456	for (i = 0; i < bwdctx + fwdctx + 1 + show_prompt * top_prompt; i++)
    457		EPRINTF(CSI_CUR_UP);
    458}
    459
    460static bool
    461search_handlekey(int c)
    462{
    463	size_t cnt;
    464
    465	switch (c) {
    466	case KEY_CTRL('I'):
    467		searchcase ^= 1;
    468		break;
    469	case KEY_PGUP:
    470		cnt = fwdctx + bwdctx + 1;
    471		selected = search_match(selected, BWD, cnt, selected, true, true);
    472		break;
    473	case KEY_PGDN:
    474		cnt = fwdctx + bwdctx + 1;
    475		selected = search_match(selected, FWD, cnt, selected, true, true);
    476		break;
    477	case KEY_CTRL('K'):
    478	case KEY_UP:
    479		selected = search_match(selected, BWD, 1, selected, true, true);
    480		break;
    481	case KEY_CTRL('L'):
    482	case KEY_DOWN:
    483		selected = search_match(selected, FWD, 1, selected, true, true);
    484		break;
    485	case 0x20 ... 0x7e:
    486		if (searchlen < sizeof(searchbuf) - 1)
    487			searchbuf[searchlen++] = (char) (c & 0xff);
    488		break;
    489	case KEY_DEL:
    490		if (searchlen) searchlen--;
    491		break;
    492	}
    493
    494	return false;
    495}
    496
    497static void
    498search_cleanup(void)
    499{
    500	size_t i;
    501
    502	for (i = 0; i < bwdctx + 1 + fwdctx; i++)
    503		EPRINTF(CSI_CLEAR_LINE "\n");
    504	for (i = 0; i < bwdctx + 1 + fwdctx; i++)
    505		EPRINTF(CSI_CUR_UP);
    506}
    507
    508static ssize_t
    509search_match(ssize_t start, int dir,
    510	size_t cnt, ssize_t fallback, bool new, bool closest)
    511{
    512	return searchmodes[searchmode].match(start, dir,
    513		cnt, fallback, new, closest);
    514}
    515
    516static ssize_t
    517search_match_linear(ssize_t start, int dir,
    518	size_t cnt, ssize_t fallback, bool new, bool closest)
    519{
    520	ssize_t index;
    521
    522	index = start + dir * (ssize_t) (new + cnt - 1);
    523	if (index < 0) return closest ? 0 : fallback;
    524	if (index >= delims_cnt)
    525		return closest ? (ssize_t) delims_cnt - 1 : fallback;
    526
    527	return index;
    528}
    529
    530static ssize_t
    531search_match_substr(ssize_t start, int dir,
    532	size_t cnt, ssize_t fallback, bool new, bool closest)
    533{
    534	const char *end, *bp;
    535	size_t i, found;
    536	ssize_t index, prev;
    537
    538	if (!searchlen)
    539		return search_match_linear(start, dir,
    540			cnt, fallback, new, closest);
    541
    542	prev = -1;
    543	found = 0;
    544	for (i = new; i < delims_cnt; i++) {
    545		index = start + dir * (ssize_t) i;
    546		if (index < 0 || index >= delims_cnt)
    547			break;
    548
    549		entry = get_entry((size_t) index);
    550		end = entry + entry_len((size_t) index);
    551
    552		for (bp = entry; *bp; bp++) {
    553			if (searchlen > end - bp) continue;
    554			if (search_eq(bp, searchbuf, searchlen)) {
    555				if (++found == cnt)
    556					return index;
    557				prev = index;
    558				break;
    559			}
    560		}
    561	}
    562
    563	return closest ? prev : fallback;
    564}
    565
    566static ssize_t
    567search_match_fuzzy(ssize_t start, int dir,
    568	size_t cnt, ssize_t fallback, bool new, bool closest)
    569{
    570	const char *end, *pos, *c;
    571	size_t i, found;
    572	ssize_t index, prev;
    573
    574	if (!searchlen)
    575		return search_match_linear(start, dir,
    576			cnt, fallback, new, closest);
    577
    578	prev = -1;
    579	found = 0;
    580	for (i = new; i < delims_cnt; i++) {
    581		index = start + dir * (ssize_t) i;
    582		if (index < 0 || index >= delims_cnt)
    583			break;
    584
    585		entry = get_entry((size_t) index);
    586		end = entry + entry_len((size_t) index);
    587
    588		pos = entry;
    589		for (c = searchbuf; c - searchbuf < searchlen; c++) {
    590			pos = search_find(pos, *c, (size_t) (end - pos));
    591			if (!pos) break;
    592			pos++;
    593		}
    594		if (c == searchbuf + searchlen) {
    595			if (++found == cnt)
    596				return index;
    597			prev = index;
    598		}
    599	}
    600
    601	return closest ? prev : fallback;
    602}
    603
    604static void
    605load(int fd)
    606{
    607	ssize_t nread;
    608	size_t i;
    609	char *c;
    610
    611	while (1) {
    612		input = addcap(input, 1, input_len + BUFSIZ, &input_cap);
    613		nread = read(fd, input + input_len, BUFSIZ);
    614		if (nread <= 0) break;
    615
    616		c = input + input_len;
    617		for (i = 0; i < (size_t) nread; i++, c++) {
    618			if (*c != delim) continue;
    619			*c = '\0';
    620			delims = addcap(delims, sizeof(size_t),
    621				delims_cnt + 1, &delims_cap);
    622			delims[delims_cnt++] = input_len + i;
    623			entry_max = MAX(entry_max, entry_len(delims_cnt-1));
    624		}
    625
    626		input_len += (size_t) nread;
    627	}
    628
    629	if (!delims_cnt && input_len
    630			|| delims_cnt && delims[delims_cnt-1] != input_len-1) {
    631		delims = addcap(delims, sizeof(size_t),
    632			delims_cnt + 1, &delims_cap);
    633		delims[delims_cnt++] = input_len;
    634		entry_max = MAX(entry_max, entry_len(delims_cnt-1));
    635	}
    636
    637	if (verbose)
    638		EPRINTF("Loaded %lu entries\n", delims_cnt);
    639}
    640
    641static void
    642run(void)
    643{
    644	struct termios prevterm, newterm = { 0 };
    645	int c;
    646
    647	if (!delims_cnt) return;
    648
    649	if (tcgetattr(0, &prevterm))
    650		die("tcgetattr:");
    651
    652	cfmakeraw(&newterm);
    653	newterm.c_oflag |= ONLCR | OPOST;
    654	if (tcsetattr(0, TCSANOW, &newterm))
    655		die("tcsetattr:");
    656
    657	EPRINTF(CSI_CUR_HIDE);
    658
    659	init = true;
    660	selected = 0;
    661	searchlen = 0;
    662	do {
    663		prompt();
    664
    665		switch ((c = readkey(stdin))) {
    666		case KEY_CTRL('C'):
    667			goto exit;
    668		case KEY_CTRL('D'):
    669			if (!multiout) goto exit;
    670			break;
    671		case KEY_CTRL('S'):
    672			searchmode = SEARCH_SUBSTR;
    673			mode = MODE_SEARCH;
    674			break;
    675		case KEY_CTRL('F'):
    676			searchmode = SEARCH_FUZZY;
    677			mode = MODE_SEARCH;
    678			break;
    679		case KEY_CTRL('Q'):
    680		case KEY_CTRL('B'):
    681			mode = MODE_BROWSE;
    682			break;
    683		case KEY_CTRL('L'):
    684			EPRINTF(CSI_CLEAR_SCREEN CSI_CUR_GOTO, 0, 0);
    685			break;
    686		case KEY_CTRL('W'):
    687			searchlen = 0;
    688			break;
    689		case KEY_CTRL('J'):
    690		case '\r':
    691			if (selected < 0) break;
    692			entry = get_entry((size_t) selected);
    693			modes[mode].cleanup();
    694			printf("%.*s\n", (int) entry_len((size_t) selected), entry);
    695			if (!multiout) goto exit;
    696			break;
    697		default:
    698			if (modes[mode].handlekey(c))
    699				goto exit;
    700			break;
    701		}
    702	} while (c >= 0);
    703
    704exit:
    705	modes[mode].cleanup();
    706
    707	EPRINTF(CSI_CUR_SHOW);
    708
    709	tcsetattr(fileno(stdin), TCSANOW, &prevterm);
    710}
    711
    712static int
    713parseopt(const char *flag, const char **args)
    714{
    715	char *end;
    716
    717	if (flag[0] && flag[1]) {
    718		EPRINTF("Invalid flag: -%s\n", flag);
    719		exit(1);
    720	}
    721
    722	switch (flag[0]) {
    723	case 'm':
    724		multiout = true;
    725		return 0;
    726	case 'v':
    727		verbose = true;
    728		return 0;
    729	case 's':
    730		mode = MODE_SEARCH;
    731		searchmode = SEARCH_SUBSTR;
    732		return 0;
    733	case 'n':
    734		show_prompt = false;
    735		return 0;
    736	case 'a':
    737		if (!*args) die("missing -a arg");
    738		fwdctx = strtoull(*args, &end, 10);
    739		if (end && *end) die("bad -a arg '%s'", *args);
    740		return 1;
    741	case 'b':
    742		if (!*args) die("missing -b arg");
    743		bwdctx = strtoull(*args, &end, 10);
    744		if (end && *end) die("bad -b arg '%s'", *args);
    745		return 1;
    746	case 'c':
    747		if (!*args) die("missing -c arg");
    748		fwdctx = bwdctx = strtoull(*args, &end, 10);
    749		if (end && *end) die("bad -c arg '%s'", *args);
    750		return 1;
    751	case 'd':
    752		if (!*args) die("missing -d arg");
    753		if (args[0][0] && args[0][1]) die("bad -d arg");
    754		delim = **args;
    755		return 1;
    756	case 't':
    757		top_prompt = true;
    758		return 0;
    759	case 'h':
    760		printf("Usage: tmenu [-h] [-m] [-a LINES] [-b LINES]");
    761		exit(0);
    762	default:
    763		die("unknown opt '%s'", *flag);
    764	}
    765
    766	return 0;
    767}
    768
    769int
    770main(int argc, const char **argv)
    771{
    772	const char **arg;
    773	int fd;
    774
    775	signal(SIGWINCH, sigwinch);
    776	setvbuf(stdout, NULL, _IONBF, 0);
    777	setvbuf(stderr, NULL, _IONBF, 0);
    778
    779	for (arg = argv + 1; *arg; arg++) {
    780		if (**arg == '-') {
    781			arg += parseopt(*arg + 1, arg + 1);
    782		} else {
    783			fd = open(*arg, O_RDONLY);
    784			if (fd < 0) die("open '%s':", *arg);
    785			load(fd);
    786			close(fd);
    787		}
    788	}
    789
    790	if (!input) {
    791		load(0);
    792		if (!freopen("/dev/tty", "r", stdin))
    793			die("freopen tty:");
    794	}
    795
    796	run();
    797}