zygo

ncurses gopher client
git clone https://hhvn.uk/zygo
git clone git://hhvn.uk/zygo
Log | Files | Refs

zygo.c (25123B)


      1 /*
      2  * zygo/zygo.c
      3  *
      4  * Copyright (c) 2022 hhvn <dev@hhvn.uk>
      5  *
      6  * Permission to use, copy, modify, and distribute this software for any
      7  * purpose with or without fee is hereby granted, provided that the above
      8  * copyright notice and this permission notice appear in all copies.
      9  *
     10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     17  *
     18  */
     19 
     20 #define _XOPEN_SOURCE_EXTENDED /* ncurses wchar wants this sometimes */
     21 #include <ncurses.h>
     22 #include <stdlib.h>
     23 #include <string.h>
     24 #include <stdarg.h>
     25 #include <libgen.h>
     26 #include <assert.h>
     27 #include <locale.h>
     28 #include <signal.h>
     29 #include <unistd.h>
     30 #include <limits.h>
     31 #include <regex.h>
     32 #include <ctype.h>
     33 #include <stdio.h>
     34 #include <wchar.h>
     35 #include <sys/wait.h>
     36 #include "zygo.h"
     37 #include "config.h"
     38 
     39 Elem *history = NULL;
     40 Elem *page = NULL;
     41 Elem *current = NULL;
     42 int insecure = 0;
     43 
     44 #define TLSOPTS "ku"
     45 
     46 struct {
     47 	int scroll;
     48 	int wantinput; /* 0 - no
     49 			* 1 - yes (with cmd)
     50 			* 2 - yes (id) */
     51 	wchar_t input[BUFLEN];
     52 	char cmd;
     53 	char arg[BUFLEN * 4]; /* UTF8 max char size: 4 bytes. 4x sizeof(input) */
     54 	int search;
     55 	regex_t regex;
     56 	int error;
     57 	char errorbuf[BUFLEN];
     58 } ui = {.scroll = 0,
     59 	.wantinput = 0,
     60 	.search = 0,
     61 	.error = 0};
     62 
     63 /*
     64  * Memory functions
     65  */
     66 void *
     67 emalloc(size_t size) {
     68 	void *mem;
     69 
     70 	if ((mem = malloc(size)) == NULL) {
     71 		perror("malloc()");
     72 		exit(EXIT_FAILURE);
     73 	}
     74 
     75 	return mem;
     76 }
     77 
     78 void *
     79 erealloc(void *ptr, size_t size) {
     80 	void *mem;
     81 
     82 	if ((mem = realloc(ptr, size)) == NULL) {
     83 		perror("realloc()");
     84 		exit(EXIT_FAILURE);
     85 	}
     86 
     87 	return mem;
     88 }
     89 
     90 char *
     91 estrdup(const char *str) {
     92 	char *ret;
     93 
     94 	if ((ret = strdup(str)) == NULL) {
     95 		perror("strdup()");
     96 		exit(EXIT_FAILURE);
     97 	}
     98 
     99 	return ret;
    100 }
    101 
    102 /*
    103  * Elem functions
    104  */
    105 void
    106 elem_free(Elem *e) {
    107 	if (e) {
    108 		free(e->desc);
    109 		free(e->selector);
    110 		free(e->server);
    111 		free(e->port);
    112 		free(e);
    113 	}
    114 }
    115 
    116 Elem *
    117 elem_create(int tls, char type, char *desc, char *selector, char *server, char *port) {
    118 	Elem *ret = emalloc(sizeof(Elem));
    119 	ret->tls = tls;
    120 #define DUP(str) str ? estrdup(str) : NULL
    121 	ret->type = type;
    122 	ret->desc = DUP(desc);
    123 	ret->selector = DUP(selector);
    124 	ret->server = DUP(server);
    125 	ret->port = DUP(port);
    126 	ret->id = ret->len = ret->lastid = 0;
    127 	ret->next = NULL;
    128 #undef DUP
    129 	return ret;
    130 }
    131 
    132 Elem *
    133 elem_dup(Elem *e) {
    134 	return (e ? elem_create(e->tls, e->type, e->desc, e->selector, e->server, e->port) : NULL);
    135 }
    136 
    137 char *
    138 elemtouri(Elem *e) {
    139 	static char ret[BUFLEN];
    140 	char type[2] = {0, 0};
    141 
    142 	ret[0] = '\0';
    143 
    144 	switch (e->type) {
    145 	case 'T':
    146 	case '8':
    147 		strlcat(ret, "teln://", sizeof(ret));
    148 		break;
    149 	case 'h':
    150 		if (strncmp(e->selector, "URL:", strlen("URL:")) == 0) {
    151 			strlcat(ret, e->selector + strlen("URL:"), sizeof(ret));
    152 			return ret;
    153 		}
    154 		else if (strncmp(e->selector, "/URL:", strlen("/URL:")) == 0) {
    155 			strlcat(ret, e->selector + strlen("URL:"), sizeof(ret));
    156 			return ret;
    157 		}
    158 		/* fallthrough */
    159 	default:
    160 		strlcat(ret, e->tls ? "gophers://" : "gopher://", sizeof(ret));
    161 		break;
    162 	}
    163 
    164 	zygo_assert(e->server);
    165 	zygo_assert(e->port);
    166 
    167 	strlcat(ret, e->server, sizeof(ret));
    168 	if (strcmp(e->port, "70") != 0) {
    169 		strlcat(ret, ":", sizeof(ret));
    170 		strlcat(ret, e->port, sizeof(ret));
    171 	}
    172 
    173 	type[0] = e->type;
    174 	strlcat(ret, "/", sizeof(ret));
    175 	strlcat(ret, type, sizeof(ret));
    176 
    177 	if (e->selector && *e->selector && strcmp(e->selector, "/") != 0)
    178 		strlcat(ret, e->selector, sizeof(ret));
    179 
    180 	return ret;
    181 }
    182 
    183 Elem *
    184 uritoelem(const char *uri) {
    185 	Elem *ret;
    186 	char *dup = estrdup(uri);
    187 	char *tmp = dup;
    188 	char *serv = NULL;
    189 	char *p;
    190 	enum {SEGSERVER, SEGTYPE, SEGSELECTOR} seg;
    191 
    192 	ret = elem_create(0, '1', NULL, NULL, NULL, NULL);
    193 
    194 	if (strncmp(tmp, "gopher://", strlen("gopher://")) == 0) {
    195 		tmp += strlen("gopher://");
    196 	} else if (strncmp(tmp, "gophers://", strlen("gophers://")) == 0) {
    197 #ifdef TLS
    198 		ret->tls = 1;
    199 		tmp += strlen("gophers://");
    200 #else
    201 		error("TLS support not compiled");
    202 		free(ret);
    203 		ret = NULL;
    204 		goto end;
    205 #endif /* TLS */
    206 	} else if (strstr(tmp, "://")) {
    207 		error("non-gopher protocol entered");
    208 		free(ret);
    209 		ret = NULL;
    210 		goto end;
    211 	}
    212 
    213 	for (p = tmp, seg = SEGSERVER; *p; p++) {
    214 		if (seg == SEGSELECTOR || *p == '\t') {
    215 			ret->selector = estrdup(p);
    216 			if (seg == SEGSERVER) {
    217 				*p = '\0';
    218 				serv = tmp;
    219 			}
    220 			break;
    221 		} else if (seg == SEGSERVER && *p == '/') {
    222 			*p = '\0';
    223 			serv = tmp;
    224 			tmp = p + 1;
    225 			seg = SEGTYPE;
    226 		} else if (seg == SEGTYPE) {
    227 			ret->type = *p;
    228 			tmp = p + 1;
    229 			break;
    230 		}
    231 	}
    232 
    233 	if (!serv && seg == SEGSERVER) {
    234 		serv = tmp;
    235 		tmp += strlen(tmp);
    236 	}
    237 
    238 	if (*serv == '[' && (p = strstr(serv + 1, "]:"))) { /* ipv6 + port */
    239 		*p = '\0';
    240 		ret->server = estrdup(serv + 1);
    241 		ret->port = estrdup(p + 2);
    242 	} else if ((p = strchr(serv, ':')) == strrchr(serv, ':') && p) { /* only one : == ipv4 + port */
    243 		*p = '\0';
    244 		ret->server = estrdup(serv);
    245 		ret->port = estrdup(p + 1);
    246 	} else { /* no port */
    247 		ret->server = estrdup(serv);
    248 		ret->port = estrdup("70");
    249 	}
    250 
    251 	if (!ret->selector)
    252 		ret->selector = estrdup(tmp);
    253 
    254 end:
    255 	free(dup);
    256 	return ret;
    257 }
    258 
    259 Elem *
    260 gophertoelem(Elem *from, const char *line) {
    261 	Elem *ret;
    262 	char *dup = estrdup(line);
    263 	char *tmp = dup;
    264 	char *p;
    265 	enum {SEGDESC, SEGSELECTOR, SEGSERVER, SEGPORT} seg;
    266 
    267 	ret = elem_create(0, *(tmp++), NULL, NULL, NULL, NULL);
    268 
    269 	for (p = tmp, seg = SEGDESC; *p; p++) {
    270 		if (*p == '\t') {
    271 			*p = '\0';
    272 			switch (seg) {
    273 			case SEGDESC:     ret->desc     = estrdup(tmp); break;
    274 			case SEGSELECTOR: ret->selector = estrdup(tmp); break;
    275 			case SEGSERVER:   ret->server   = estrdup(tmp); break;
    276 			case SEGPORT:     ret->port     = estrdup(tmp); break;
    277 			}
    278 			tmp = p + 1;
    279 			seg++;
    280 		}
    281 	}
    282 
    283 	/* ret->port will only be set on gopher+ menus with 
    284 	 * the above loop, set it here for non-gopher+ */
    285 	if (!ret->port)
    286 		ret->port = estrdup(tmp);
    287 	if (from && from->tls && ret->server && ret->port &&
    288 			strcmp(ret->server, from->server) == 0 &&
    289 			strcmp(ret->port, from->port) == 0)
    290 		ret->tls = 1;
    291 	else
    292 		ret->tls = 0;
    293 
    294 	free(dup);
    295 
    296 	if (ret->desc != NULL &&
    297 			ret->server != NULL &&
    298 			ret->port != NULL)
    299 		return ret;
    300 
    301 	free(ret->desc);
    302 	free(ret->selector);
    303 	free(ret->server);
    304 	free(ret->port);
    305 	ret->type = '3';
    306 	ret->desc = estrdup("invalid gopher menu element");
    307 	ret->selector = estrdup("Err");
    308 	ret->server = estrdup("Err");
    309 	ret->port = estrdup("Err");
    310 	return ret;
    311 }
    312 
    313 /*
    314  * List functions
    315  */
    316 void
    317 list_free(Elem **l) {
    318 	Elem *prev, *p;
    319 
    320 	if (!l || !*l)
    321 		return;
    322 	for (prev = *l, p = prev->next; p; p = p->next) {
    323 		elem_free(prev);
    324 		prev = p;
    325 	}
    326 	*l = NULL;
    327 }
    328 
    329 void
    330 list_append(Elem **l, Elem *e) {
    331 	Elem *elem, *p;
    332 
    333 	zygo_assert(l);
    334 	elem = elem_dup(e);
    335 
    336 	if (!*l) {
    337 		(*l) = elem;
    338 		(*l)->len = 1;
    339 		(*l)->lastid = 0; /* incremented later */
    340 	} else {
    341 		for (p = *l; p && p->next; p = p->next);
    342 		p->next = elem;
    343 		(*l)->len++;
    344 	}
    345 
    346 	if (elem->type != 'i' && elem->type != '3')
    347 		elem->id = ++(*l)->lastid;
    348 }
    349 
    350 Elem *
    351 list_get(Elem **l, size_t elem) {
    352 	Elem *p;
    353 	if (!l || !(*l) || (*l)->len == 0 || elem >= (*l)->len)
    354 		return NULL;
    355 	for (p = *l; p && elem; elem--, p = p->next);
    356 	return p;
    357 }
    358 
    359 Elem *
    360 list_idget(Elem **l, size_t id) {
    361 	Elem *p;
    362 	if (!l || !(*l) || (*l)->len == 0 || id > (*l)->lastid)
    363 		return NULL;
    364 	for (p = *l; p && id; p = p->next)
    365 		if (p->type != 'i' && p->type != '3')
    366 			if (!--id)
    367 				break;
    368 	return p;
    369 }
    370 
    371 size_t
    372 list_len(Elem **l) {
    373 	if (!l || !(*l))
    374 		return 0;
    375 	return (*l)->len;
    376 }
    377 
    378 void
    379 list_rev(Elem **l) {
    380 	Elem *p, *prev, *next;
    381 	size_t len, lastid;
    382 
    383 	if (!l || !*l)
    384 		return;
    385 
    386 	len = (*l)->len;
    387 	lastid = (*l)->lastid;
    388 
    389 	for (p = *l, prev = NULL; p; p = next) {
    390 		next = p->next;
    391 		p->next = prev;
    392 		prev = p;
    393 		if (p->id)
    394 			p->id = lastid - p->id + 1;
    395 	}
    396 
    397 	prev->len = len;
    398 	prev->lastid = lastid;
    399 
    400 	*l = prev;
    401 }
    402 
    403 /*
    404  * Misc functions
    405  */
    406 int
    407 readline(char *buf, size_t count) {
    408 	size_t i = 0;
    409 	char c = 0;
    410 
    411 	while (i < count && c != '\n') {
    412 		if (net_read(&c, sizeof(char)) < 1)
    413 			return 0;
    414 		buf[i++] = c;
    415 	}
    416 
    417 	buf[i - 1] = '\0';
    418 	return 1;
    419 }
    420 
    421 int
    422 go(Elem *e, int mhist, int notls) {
    423 	char line[BUFLEN];
    424 	char *uri;
    425 	char *pstr;
    426 	Elem *elem;
    427 	Elem *dup = elem_dup(e); /* elem may be part of page */
    428 	Elem missing = {0, '3', "Full contents not received."};
    429 	int ret;
    430 	int gotall = 0;
    431 	pid_t pid;
    432 
    433 	if (!e) return -1;
    434 
    435 	if (dup->type != '0' && dup->type != '1' && dup->type != '7' && dup->type != '+') {
    436 		/* call mario */
    437 		uri = elemtouri(e);
    438 
    439 		if (!parallelplumb)
    440 			endwin();
    441 
    442 		if ((pid = fork()) == 0) {
    443 			if (parallelplumb) {
    444 				close(1);
    445 				close(2);
    446 			}
    447 			execlp(plumber, plumber, uri, NULL);
    448 		}
    449 		zygo_assert(pid != -1);
    450 
    451 		if (!parallelplumb) {
    452 			waitpid(pid, NULL, 0);
    453 			fprintf(stderr, "Press enter...");
    454 			fread(&line, sizeof(char), 1, stdin);
    455 			initscr();
    456 		}
    457 		return -1;
    458 	}
    459 
    460 	if (dup->type == '7' && !strchr(dup->selector, '\t')) {
    461 		if ((pstr = prompt("Query: ", 0)) == NULL) {
    462 			elem_free(dup);
    463 			return -1;
    464 		}
    465 
    466 		free(dup->selector);
    467 		dup->selector = emalloc(strlen(e->selector) + strlen(pstr) + 2);
    468 		snprintf(dup->selector, strlen(e->selector) + strlen(pstr) + 2,
    469 				"%s\t%s", e->selector, pstr);
    470 	}
    471 
    472 	move(LINES - 1, 0);
    473 	clrtoeol();
    474 #ifdef TLS
    475 	if (!dup->tls && autotls && !notls &&
    476 			(!current || strcmp(current->server, dup->server) != 0)) {
    477 		dup->tls = 1;
    478 		printw("Attempting a TLS connection with %s:%s", dup->server, dup->port);
    479 	} else {
    480 #endif /* TLS */
    481 		printw("Connecting to %s:%s", dup->server, dup->port);
    482 #ifdef TLS
    483 	}
    484 #endif /* TLS */
    485 	refresh();
    486 
    487 	if ((ret = net_connect(dup, e->tls != dup->tls)) == -1) {
    488 		if (dup->tls && dup->tls == e->tls) {
    489 			timeout(stimeout * 1000);
    490 			pstr = prompt("TLS failed. Retry in cleartext (y/n)? ", 1);
    491 			if (pstr && tolower(*pstr) == 'y') {
    492 				dup->tls = 0;
    493 				ui.error = 0; /* hide the TLS error */
    494 				ret = go(dup, mhist, 1);
    495 			}
    496 			timeout(-1);
    497 		} else if (dup->tls) {
    498 			dup->tls = 0;
    499 			ret = go(dup, mhist, 1);
    500 		}
    501 
    502 		elem_free(dup);
    503 		return ret;
    504 	}
    505 
    506 	net_write(dup->selector, strlen(dup->selector));
    507 	net_write("\r\n", 2);
    508 
    509 	list_free(&page);
    510 	while (readline(line, sizeof(line))) {
    511 		if (strcmp(line, ".\r") == 0) {
    512 			gotall = 1;
    513 		} else {
    514 			if (line[strlen(line) - 1] == '\r')
    515 				line[strlen(line) - 1] = '\0';
    516 			if (dup->type == '0')
    517 				elem = elem_create(0, 'i', line, NULL, NULL, NULL);
    518 			else
    519 				elem = gophertoelem(dup, line);
    520 			list_append(&page, elem);
    521 			elem_free(elem);
    522 		}
    523 	}
    524 
    525 	if (!gotall && dup->type != '0')
    526 		list_append(&page, &missing);
    527 
    528 	elem_free(current);
    529 	current = dup;
    530 	if (mhist)
    531 		list_append(&history, current);
    532 
    533 	ui.scroll = 0;
    534 	if (ui.search) {
    535 		regfree(&ui.regex);
    536 		ui.search = 0;
    537 	}
    538 
    539 	return 0;
    540 }
    541 
    542 int
    543 digits(int i) {
    544 	int ret = 0;
    545 
    546 	do {
    547 		ret++;
    548 		i /= 10;
    549 	} while (i != 0);
    550 
    551 	return ret;
    552 }
    553 
    554 /*
    555  * UI functions
    556  */
    557 void
    558 error(char *format, ...) {
    559 	va_list ap;
    560 
    561 	ui.error = 1;
    562 
    563 	va_start(ap, format);
    564 	vsnprintf(ui.errorbuf, sizeof(ui.errorbuf), format, ap);
    565 	va_end(ap);
    566 
    567 	draw_bar();
    568 }
    569 
    570 Scheme *
    571 getscheme(Elem *e) {
    572 	char type;
    573 	int i;
    574 
    575 	type = e->type;
    576 	if (type == 'h' && strstr(e->selector, "URL:"))
    577 		type = EXTR;
    578 
    579 	/* Try to get scheme from markdown header */
    580 	if (type == 'i' && mdhilight) {
    581 		/* 4+ matches MDH4 */
    582 		if (strncmp(e->desc, "####", 4) == 0)
    583 			type = MDH4;
    584 		else if (strncmp(e->desc, "###", 3) == 0)
    585 			type = MDH3;
    586 		else if (strncmp(e->desc, "##", 2) == 0)
    587 			type = MDH2;
    588 		else if (strncmp(e->desc, "#", 1) == 0)
    589 			type = MDH1;
    590 	}
    591 
    592 	for (i = 0; ; i++)
    593 		if (scheme[i].type == type || scheme[i].type == DEFL)
    594 			return &scheme[i];
    595 }
    596 
    597 void
    598 find(int backward) {
    599 	enum {mfirst, mclose, mlast};
    600 	struct {
    601 		size_t pos;
    602 		int found;
    603 	} matches[] = {
    604 		[mfirst] = {.found = 0},
    605 		[mclose] = {.found = 0},
    606 		[mlast]  = {.found = 0},
    607 	};
    608 	size_t i;
    609 	size_t want;
    610 	Elem *e;
    611 
    612 	if (!ui.search) {
    613 		error("no search");
    614 		return;
    615 	}
    616 
    617 	for (i = 0, e = page; i < list_len(&page); i++, e = e->next) {
    618 		if (regexec(&ui.regex, e->desc, 0, NULL, 0) == 0) {
    619 			matches[mlast].found = 1;
    620 			matches[mlast].pos = i;
    621 			if (!matches[mfirst].found) {
    622 				matches[mfirst].found = 1;
    623 				matches[mfirst].pos = i;
    624 			}
    625 			if (!matches[mclose].found && ((backward && i < ui.scroll) || (!backward && i > ui.scroll))) {
    626 				matches[mclose].found = 1;
    627 				matches[mclose].pos = i;
    628 			}
    629 		}
    630 	}
    631 
    632 	if (matches[mfirst].found == 0 &&
    633 			matches[mclose].found == 0 &&
    634 			matches[mlast].found == 0) {
    635 		error("no match");
    636 		return;
    637 	}
    638 
    639 	if (matches[mclose].found)
    640 		want = matches[mclose].pos;
    641 	else if (backward && matches[mlast].found)
    642 		want = matches[mlast].pos;
    643 	else
    644 		want = matches[mfirst].pos;
    645 
    646 	ui.scroll = want;
    647 }
    648 
    649 int
    650 draw_line(Elem *e, int nwidth) {
    651 	int y, x, len;
    652 	wchar_t *mbdesc, *p;
    653 
    654 	if (nwidth)
    655 		attron(COLOR_PAIR(PAIR_EID));
    656 
    657 	if (e->type != 'i' && e->type != '3')
    658 		printw("%1$ *2$ld ", e->id, nwidth + 1);
    659 	else if (nwidth)
    660 		printw("%1$ *2$s ", "", nwidth + 1);
    661 
    662 	if (nwidth) {
    663 		attroff(A_COLOR);
    664 		attron(COLOR_PAIR(getscheme(e)->pair));
    665 		printw("%s ", getscheme(e)->name);
    666 		attroff(A_COLOR);
    667 		printw("%s ", normsep);
    668 	} else {
    669 		attroff(A_COLOR);
    670 	}
    671 
    672 	if (ui.search && regexec(&ui.regex, e->desc, 0, NULL, 0) == 0)
    673 		attron(A_REVERSE);
    674 
    675 	if (mdhilight && strncmp(e->desc, "#", 1) == 0) {
    676 		attron(A_BOLD);
    677 		attron(COLOR_PAIR(getscheme(e)->pair));
    678 		attroff(A_BOLD);
    679 	}
    680 
    681 	len = mbstowcs(NULL, e->desc, 0) + 1;
    682 	mbdesc = emalloc(len * sizeof(wchar_t*));
    683 	mbstowcs(mbdesc, e->desc, len);
    684 
    685 	getyx(stdscr, y, x);
    686 	for (p = mbdesc; *p; p++) {
    687 		if (*p == L'\t')
    688 			x += 8;
    689 		else
    690 			x++;
    691 		if (x >= COLS) {
    692 			attron(A_REVERSE);
    693 			printw("%s", toolong);
    694 			goto end;
    695 		}
    696 		addnwstr(p, 1);
    697 	}
    698 
    699 	printw("\n");
    700 end:
    701 	free(mbdesc);
    702 	attroff(A_REVERSE);
    703 	return y + 1;
    704 }
    705 
    706 void
    707 draw_page(void) {
    708 	int y = 0, i;
    709 	int nwidth;
    710 	Elem *e;
    711 
    712 	attroff(A_COLOR);
    713 	if (page) {
    714 		if (!current || current->type != '0')
    715 			nwidth = digits(page->lastid);
    716 		else
    717 			nwidth = 0;
    718 		move(0, 0);
    719 		if (ui.scroll > list_len(&page))
    720 			ui.scroll = 0;
    721 		for (i = ui.scroll, e = list_get(&page, i); i <= list_len(&page) - 1 && y != LINES - 1; i++, e = e->next)
    722 			y = draw_line(e, nwidth);
    723 		for (; y < LINES - 1; y++) {
    724 			move(y, 0);
    725 			clrtoeol();
    726 		}
    727 	}
    728 }
    729 
    730 void
    731 draw_bar(void) {
    732 	int savey, savex, x;
    733 
    734 	move(LINES - 1, 0);
    735 	clrtoeol();
    736 	if (current) {
    737 		attron(COLOR_PAIR(PAIR_URI));
    738 		printw(" %s ", elemtouri(current));
    739 	}
    740 	attron(COLOR_PAIR(PAIR_BAR));
    741 	printw(" ");
    742 	if (ui.error) {
    743 		curs_set(0);
    744 		attron(COLOR_PAIR(PAIR_ERR));
    745 		printw("%s", ui.errorbuf);
    746 	} else if (ui.wantinput) {
    747 		curs_set(1);
    748 		if (ui.cmd) {
    749 			attron(COLOR_PAIR(PAIR_CMD));
    750 			printw("%c", ui.cmd);
    751 		}
    752 		attron(COLOR_PAIR(PAIR_ARG));
    753 		printw("%s", ui.arg);
    754 	} else curs_set(0);
    755 
    756 	attron(COLOR_PAIR(PAIR_BAR));
    757 	getyx(stdscr, savey, savex);
    758 	for (x = savex; x < COLS; x++)
    759 		addch(' ');
    760 	move(savey, savex);
    761 }
    762 
    763 void
    764 manpage(void) {
    765 	pid_t pid;
    766 	int status;
    767 	char buf;
    768 
    769 	endwin();
    770 
    771 	pid = fork();
    772 	if (pid == 0)
    773 		execlp("man", "man", "zygo", NULL);
    774 	assert(pid != -1);
    775 	waitpid(pid, &status, 0);
    776 	if (WEXITSTATUS(status) != 0) {
    777 		fprintf(stderr, "%s", "could not find manpage, press enter to continue...");
    778 		fread(&buf, sizeof(char), 1, stdin);
    779 	}
    780 
    781 	initscr();
    782 	draw_page();
    783 	draw_bar();
    784 }
    785 
    786 /* 0 - clear
    787  * KEY_BACKSPACE - remove character
    788  * other - append character */
    789 void
    790 input(int c) {
    791 	static size_t il = 0;
    792 
    793 	if (!c) {
    794 		ui.input[il = 0] = '\0';
    795 		ui.arg[0] = '\0';
    796 		return;
    797 	} else if (c == KEY_BACKSPACE) {
    798 		ui.input[--il] = '\0';
    799 	} else if (il == sizeof(ui.input)) {
    800 		return;
    801 	} else {
    802 		ui.input[il++] = c;
    803 		ui.input[il] = '\0';
    804 	}
    805 
    806 	wcstombs(ui.arg, ui.input, sizeof(ui.arg));
    807 }
    808 
    809 char *
    810 prompt(char *prompt, size_t count) {
    811 	wint_t c;
    812 	int ret;
    813 	int x, y;
    814 
    815 	attrset(A_NORMAL);
    816 	input(0);
    817 	curs_set(1);
    818 	goto start;
    819 	while ((ret = get_wch(&c)) != ERR) {
    820 		if (c == KEY_RESIZE) {
    821 start:
    822 			draw_page();
    823 			move(LINES - 1, 0);
    824 			clrtoeol();
    825 			printw("%s%s", prompt, ui.arg);
    826 		} else if (c == 27 /* escape */) {
    827 			return NULL;
    828 		} else if (c == '\n') {
    829 			goto end;
    830 		} else if (c == KEY_BACKSPACE || c == 127) {
    831 			if (ui.input[0]) {
    832 				getyx(stdscr, y, x);
    833 				move(LINES - 1, x - 1);
    834 				addch(' ');
    835 				move(LINES - 1, x - 1);
    836 				refresh();
    837 				input(KEY_BACKSPACE);
    838 			}
    839 		} else if (c >= 32 && c < KEY_CODE_YES) {
    840 			addnwstr((wchar_t *)&c, 1);
    841 			input(c);
    842 		}
    843 
    844 		if (count && wcslen(ui.input) == count)
    845 			goto end;
    846 	}
    847 
    848 end:
    849 	return ui.arg;
    850 }
    851 
    852 Elem *
    853 strtolink(char *str) {
    854 	if (atoi(str) > page->lastid || atoi(str) < 0) {
    855 		error("no such link: %s", str);
    856 		return NULL;
    857 	}
    858 
    859 	return list_idget(&page, atoi(str));
    860 }
    861 
    862 void
    863 yank(Elem *e) {
    864 	char *uri, *sh;
    865 	int pfd[2];
    866 	int status;
    867 	pid_t pid;
    868 
    869 	uri = elemtouri(e);
    870 
    871 	zygo_assert(pipe(pfd) != -1);
    872 	zygo_assert((pid = fork()) != -1);
    873 
    874 	if (pid == 0) {
    875 		close(0);
    876 		close(1);
    877 		close(2);
    878 		close(pfd[1]);
    879 		dup2(pfd[0], 0);
    880 		execlp(yanker, yanker, NULL);
    881 	}
    882 
    883 	close(pfd[0]);
    884 	write(pfd[1], uri, strlen(uri));
    885 	close(pfd[1]);
    886 
    887 	waitpid(pid, &status, 0);
    888 	if (WEXITSTATUS(status) != 0)
    889 		error("could not execute '%s' for yanking", yanker);
    890 
    891 	free(uri);
    892 }
    893 
    894 void
    895 pagescroll(int lines) {
    896 	if (lines > 0 && list_len(&page) > LINES - 1) {
    897 		ui.scroll += lines;
    898 		if (ui.scroll > list_len(&page) - LINES)
    899 			ui.scroll = list_len(&page) - LINES + 1;
    900 	} else if (lines < 0) {
    901 		ui.scroll += lines;
    902 		if (ui.scroll < 0)
    903 			ui.scroll = 0;
    904 	} /* else intentionally left blank */
    905 	draw_page();
    906 }
    907 
    908 void
    909 idgo(size_t id) {
    910 	if (id > page->lastid || id < 1)
    911 		error("no such link: %d", id);
    912 	else
    913 		go(list_idget(&page, id), 1, 0);
    914 }
    915 
    916 int
    917 wantnum(char cmd) {
    918 	return (!ui.cmd || ui.cmd == BIND_DISPLAY || ui.cmd == BIND_YANK);
    919 }
    920 
    921 int
    922 acceptkey(char cmd, int key) {
    923 	if (wantnum(cmd))
    924 		return isdigit(key);
    925 	return key >= 32 && key < KEY_CODE_YES;
    926 }
    927 
    928 /*
    929  * Main loop
    930  */
    931 void
    932 run(void) {
    933 	wint_t c;
    934 	int ret;
    935 	Elem *e;
    936 	char tmperror[BUFLEN];
    937 
    938 	draw_page();
    939 	draw_bar();
    940 
    941 	/* get_wch does refresh() for us */
    942 	while ((ret = get_wch(&c)) != ERR) {
    943 		if (ui.error && c != KEY_RESIZE)
    944 			ui.error = 0;
    945 
    946 		if (c == KEY_RESIZE) {
    947 			draw_page();
    948 			draw_bar();
    949 		} else if (ui.wantinput) {
    950 			if (c == 27 /* escape */) {
    951 				ui.wantinput = 0;
    952 			} else if (c == '\n') {
    953 submit:
    954 				switch (ui.cmd) {
    955 				case BIND_URI:
    956 					e = uritoelem(ui.arg);
    957 					go(e, 1, 0);
    958 					elem_free(e);
    959 					break;
    960 				case BIND_DISPLAY:
    961 					if ((e = strtolink(ui.arg))) {
    962 						move(LINES - 1, 0);
    963 						attroff(A_COLOR);
    964 						clrtoeol();
    965 						printw("%s", elemtouri(list_idget(&page, atoi(ui.arg))));
    966 						curs_set(0);
    967 						getch(); /* wait */
    968 					}
    969 					break;
    970 				case BIND_SEARCH:
    971 				case BIND_SEARCH_BACK:
    972 					if (ui.search) {
    973 						regfree(&ui.regex);
    974 						ui.search = 0;
    975 					}
    976 
    977 					if (ui.input[0] != '\0') {
    978 						if ((ret = regcomp(&ui.regex, ui.arg, regexflags)) != 0) {
    979 							regerror(ret, &ui.regex, (char *)&tmperror, sizeof(tmperror));
    980 							error("could not compile regex '%s': %s", ui.arg, tmperror);
    981 						} else {
    982 							ui.search = 1;
    983 							find(ui.cmd == BIND_SEARCH_BACK ? 1 : 0);
    984 						}
    985 					}
    986 					break;
    987 				case BIND_APPEND:
    988 					e = elem_dup(current);
    989 					e->selector = erealloc(e->selector, strlen(e->selector) + strlen(ui.arg) + 1);
    990 					/* should be safe.. I think */
    991 					strcat(e->selector, ui.arg);
    992 					go(e, 1, 0);
    993 					elem_free(e);
    994 					break;
    995 				case BIND_YANK:
    996 					if ((e = strtolink(ui.arg)))
    997 						yank(e);
    998 					break;
    999 				case '\0': /* links */
   1000 					idgo(atoi(ui.arg));
   1001 				}
   1002 				ui.wantinput = 0;
   1003 				draw_page();
   1004 			} else if (c == KEY_BACKSPACE || c == 127) {
   1005 				if ((ui.cmd && !ui.input[0]) || (!ui.cmd && !ui.input[1]))
   1006 					ui.wantinput = 0;
   1007 				else
   1008 					input(KEY_BACKSPACE);
   1009 			} else if (ui.cmd == BIND_YANK && c == BIND_YANK && !ui.input[0]) {
   1010 				ui.wantinput = 0;
   1011 				yank(current);
   1012 			} else if (acceptkey(ui.cmd, c)) {
   1013 				input(c);
   1014 				if (wantnum(ui.cmd) && atoi(ui.arg) * 10 > page->lastid)
   1015 					goto submit;
   1016 			}
   1017 			draw_bar();
   1018 		} else {
   1019 			if ((c == BIND_RELOAD || c == BIND_ROOT || c == BIND_APPEND || c == BIND_YANK) &&
   1020 					(!current || !current->server || !current->port)) {
   1021 				error("%c command can only be used on remote gopher menus", c);
   1022 				continue;
   1023 			}
   1024 
   1025 			switch (c) {
   1026 			case KEY_DOWN:
   1027 			case BIND_DOWN:
   1028 				pagescroll(1);
   1029 				break;
   1030 			case 4: /* ^D */
   1031 			case 6: /* ^F */
   1032 				pagescroll((int)(LINES / 2));
   1033 				break;
   1034 			case KEY_UP:
   1035 			case BIND_UP:
   1036 				pagescroll(-1);
   1037 				break;
   1038 			case 21: /* ^U */
   1039 			case 2:  /* ^B */
   1040 				pagescroll(-(int)(LINES / 2));
   1041 				break;
   1042 			case 3: /* ^C */
   1043 			case BIND_QUIT:
   1044 				endwin();
   1045 				exit(EXIT_SUCCESS);
   1046 			case BIND_BACK:
   1047 				if (history && history->next) {
   1048 					for (e = history; e; e = e->next)
   1049 						if (e->next && !e->next->next)
   1050 							break;
   1051 					go(e, 0, 0);
   1052 					free(e->next);
   1053 					e->next = NULL;
   1054 					draw_page();
   1055 					draw_bar();
   1056 				} else {
   1057 					error("no previous history");
   1058 				}
   1059 				break;
   1060 			case BIND_RELOAD:
   1061 				go(current, 0, 0);
   1062 				draw_page();
   1063 				draw_bar();
   1064 				break;
   1065 			case BIND_TOP:
   1066 				pagescroll(INT_MIN);
   1067 				break;
   1068 			case BIND_BOTTOM:
   1069 				pagescroll(INT_MAX);
   1070 				break;
   1071 			case BIND_SEARCH_NEXT:
   1072 			case BIND_SEARCH_PREV:
   1073 				find(c == BIND_SEARCH_PREV ? 1 : 0);
   1074 				draw_page();
   1075 				break;
   1076 			case BIND_ROOT:
   1077 				e = elem_dup(current);
   1078 				free(e->selector);
   1079 				e->selector = strdup("");
   1080 				go(e, 1, 0);
   1081 				elem_free(e);
   1082 				draw_page();
   1083 				draw_bar();
   1084 				break;
   1085 			case BIND_HELP:
   1086 				manpage();
   1087 				break;
   1088 			case BIND_HISTORY:
   1089 				if (history) {
   1090 					elem_free(current);
   1091 					current = NULL;
   1092 					list_free(&page);
   1093 					for (e = history; e; e = e->next) {
   1094 						free(e->desc);
   1095 						e->desc = elemtouri(e);
   1096 						list_append(&page, e);
   1097 					}
   1098 					list_rev(&page);
   1099 					draw_bar();
   1100 					draw_page();
   1101 				} else {
   1102 					error("no history");
   1103 				}
   1104 				break;
   1105 			/* link numbers */
   1106 			case '0': case '1': case '2': case '3': case '4':
   1107 			case '5': case '6': case '7': case '8': case '9':
   1108 				ui.wantinput = 1;
   1109 				ui.cmd = '\0';
   1110 				input(0);
   1111 				input(c);
   1112 				if (atoi(ui.arg) * 10 > page->lastid) {
   1113 					idgo(atoi(ui.arg));
   1114 					ui.wantinput = 0;
   1115 					draw_page();
   1116 				}
   1117 				draw_bar();
   1118 				break;
   1119 			/* commands with arg */
   1120 			case BIND_URI:
   1121 			case BIND_DISPLAY:
   1122 			case BIND_SEARCH:
   1123 			case BIND_SEARCH_BACK:
   1124 			case BIND_APPEND:
   1125 			case BIND_YANK:
   1126 				ui.cmd = (char)c;
   1127 				ui.wantinput = 1;
   1128 				input(0);
   1129 				draw_bar();
   1130 				break;
   1131 			case '\n':
   1132 			case 27: /* escape */
   1133 			case KEY_BACKSPACE:
   1134 				break;
   1135 			default:
   1136 				error("not bound");
   1137 				break;
   1138 			}
   1139 		}
   1140 	}
   1141 }
   1142 
   1143 void
   1144 sighandler(int signal) {
   1145 	switch (signal) {
   1146 	case SIGCHLD:
   1147 		while (waitpid(-1, NULL, WNOHANG) == 0);
   1148 		break;
   1149 	}
   1150 }
   1151 
   1152 void
   1153 usage(char *argv0) {
   1154 #ifdef TLS
   1155 #define OPTS "-Pv" TLSOPTS
   1156 #else
   1157 #define OPTS "-Pv"
   1158 #endif /* TLS */
   1159 	fprintf(stderr, "usage: %s [%s] [-p plumber] [-y yanker] [uri]\n", basename(argv0), OPTS);
   1160 	exit(EXIT_FAILURE);
   1161 #undef OPTS
   1162 }
   1163 
   1164 int
   1165 main(int argc, char *argv[]) {
   1166 	Elem *target = NULL;
   1167 	Elem err = {0, 0, NULL, NULL, NULL, NULL, 0};
   1168 	char *s;
   1169 	int i;
   1170 
   1171 	for (i = 1; i < argc; i++) {
   1172 		if ((*argv[i] == '-' && *(argv[i]+1) == '\0') ||
   1173 				(*argv[i] != '-' && target)) {
   1174 			usage(argv[0]);
   1175 		} else if (*argv[i] == '-') {
   1176 			for (s = argv[i]+1; *s; s++) {
   1177 #ifndef TLS
   1178 				if (strchr(TLSOPTS, *s)) {
   1179 					fprintf(stderr, "-%c: TLS support not compiled\n", *s);
   1180 					exit(EXIT_FAILURE);
   1181 				}
   1182 #endif /* TLS */
   1183 				switch (*s) {
   1184 				case 'k':
   1185 					insecure = 1;
   1186 					break;
   1187 				case 'p':
   1188 					if (*(s+1)) {
   1189 						plumber = s + 1;
   1190 						s += strlen(s) - 1;
   1191 					} else if (i + 1 != argc) {
   1192 						plumber = argv[++i];
   1193 					} else {
   1194 						usage(argv[0]);
   1195 					}
   1196 					break;
   1197 				case 'y':
   1198 					if (*(s+1)) {
   1199 						yanker = s + 1;
   1200 						s += strlen(s) - 1;
   1201 					} else if (i + 1 != argc) {
   1202 						yanker = argv[++i];
   1203 					} else {
   1204 						usage(argv[0]);
   1205 					}
   1206 					break;
   1207 				case 'P':
   1208 					parallelplumb = 1;
   1209 					break;
   1210 				case 'u':
   1211 					autotls = 1;
   1212 					break;
   1213 				case 'v':
   1214 					fprintf(stderr, "zygo %s\n", COMMIT);
   1215 					exit(EXIT_SUCCESS);
   1216 				default:
   1217 					usage(argv[0]);
   1218 				}
   1219 			}
   1220 		} else {
   1221 			target = uritoelem(argv[argc-1]);
   1222 		}
   1223 	}
   1224 
   1225 	if (!page) {
   1226 		if (ui.error) {
   1227 			err.type = '3';
   1228 			err.desc = ui.errorbuf;
   1229 			list_append(&page, &err);
   1230 			err.type = 'i';
   1231 			err.desc = "";
   1232 			list_append(&page, &err);
   1233 		}
   1234 
   1235 		for (i = 0; i < sizeof(start_page) / sizeof(start_page[0]); i++)
   1236 			list_append(&page, &start_page[i]);
   1237 	}
   1238 
   1239 	setlocale(LC_ALL, "");
   1240 	initscr();
   1241 	noecho();
   1242 	start_color();
   1243 	use_default_colors();
   1244 	keypad(stdscr, TRUE);
   1245 	set_escdelay(10);
   1246 
   1247 	signal(SIGALRM, sighandler);
   1248 	signal(SIGCHLD, sighandler);
   1249 
   1250 	init_pair(PAIR_BAR, bar_pair[0], bar_pair[1]);
   1251 	init_pair(PAIR_URI, uri_pair[0], uri_pair[1]);
   1252 	init_pair(PAIR_CMD, cmd_pair[0], cmd_pair[1]);
   1253 	init_pair(PAIR_ARG, arg_pair[0], arg_pair[1]);
   1254 	init_pair(PAIR_ERR, err_pair[0], err_pair[1]);
   1255 	init_pair(PAIR_EID, eid_pair[0], eid_pair[1]);
   1256 	for (i = 0; i == 0 || scheme[i - 1].type; i++) {
   1257 		scheme[i].pair = i + PAIR_SCHEME;
   1258 		init_pair(scheme[i].pair, scheme[i].fg, -1);
   1259 	}
   1260 
   1261 	if (target)
   1262 		go(target, 1, 0);
   1263 
   1264 	run();
   1265 
   1266 	endwin();
   1267 }