cgmnlm

colorful gemini line mode browser
git clone https://git.clttr.info/cgmnlm.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

gmnlm.c (38339B)


      1 #include <assert.h>
      2 #include <bearssl.h>
      3 #include <ctype.h>
      4 #include <errno.h>
      5 #include <fcntl.h>
      6 #include <getopt.h>
      7 #include <gmni/certs.h>
      8 #include <gmni/gmni.h>
      9 #include <gmni/tofu.h>
     10 #include <gmni/url.h>
     11 #include <libgen.h>
     12 #include <limits.h>
     13 #include <regex.h>
     14 #include <stdbool.h>
     15 #include <stdio.h>
     16 #include <stdlib.h>
     17 #include <string.h>
     18 #include <sys/ioctl.h>
     19 #include <sys/stat.h>
     20 #include <sys/types.h>
     21 #include <sys/wait.h>
     22 #include <termios.h>
     23 #include <unistd.h>
     24 #include "util.h"
     25 
     26 #define ANSI_COLOR_RED     "\x1b[91m"
     27 #define ANSI_COLOR_GREEN   "\x1b[92m"
     28 #define ANSI_COLOR_YELLOW  "\x1b[93m"
     29 #define ANSI_COLOR_BLUE    "\x1b[94m"
     30 #define ANSI_COLOR_MAGENTA "\x1b[35m"
     31 #define ANSI_COLOR_LMAGENTA "\x1b[95m"
     32 #define ANSI_COLOR_CYAN    "\x1b[36m"
     33 #define ANSI_COLOR_LCYAN   "\x1b[96m"
     34 #define ANSI_COLOR_GRAY    "\x1b[37m"
     35 #define ANSI_COLOR_RESET   "\x1b[0m"
     36 
     37 struct link {
     38 	char *url;
     39 	struct link *next;
     40 };
     41 
     42 struct history {
     43 	char *url;
     44 	struct history *prev, *next;
     45 };
     46 
     47 #define REDIRS_UNLIMITED -1
     48 #define REDIRS_ASK -2
     49 
     50 struct browser {
     51 	bool pagination, unicode, alttext, autoopen;
     52 	int max_width;
     53 	int max_redirs;
     54 	struct gemini_options opts;
     55 	struct gemini_tofu tofu;
     56 	enum tofu_action tofu_mode;
     57 
     58 	FILE *tty;
     59 	char *meta;
     60 	char *plain_url;
     61 	char *page_title;
     62 	struct Curl_URL *url;
     63 	struct link *links;
     64 	struct history *history;
     65 	bool running;
     66 
     67 	bool searching;
     68 	regex_t regex;
     69 };
     70 
     71 enum prompt_result {
     72 	PROMPT_AGAIN,
     73 	PROMPT_MORE,
     74 	PROMPT_QUIT,
     75 	PROMPT_ANSWERED,
     76 	PROMPT_NEXT,
     77 };
     78 
     79 const char *default_bookmarks =
     80 	"# Welcome to cgmnlm\n\n"
     81 	"Links:\n\n"
     82 	"=> gemini://gmn.clttr.info/cgmnln.gmi The colorful gemini line mode client\n"
     83 	"=> gemini://gemini.circumlunar.space The gemini protocol\n"
     84 	"=> gemini://geminispace.info/search/ search in geminispace\n\n"
     85 	"This file can be found at %s and may be edited at your pleasure.\n\n"
     86 	"Bookmarks:\n"
     87 	;
     88 
     89 const char *help_msg =
     90 	"The following commands are available:\n\n"
     91 	"<Enter>\t\tRead more lines (if available)\n"
     92 	"<url>\t\tGo to url\n"
     93 	"[N]\t\tFollow Nth link\n"
     94 	"p[N]\t\tPrint URL of Nth link\n"
     95 	"e[N]\t\tSend URL of current page or Nth link to external default program\n"
     96 	"t[N]\t\tDownload content of current page or Nth link to a temporary file\n"
     97 	"b[N]\t\tJump back N entries in history, default 1\n"
     98 	"f[N]\t\tJump forward N entries in history, default 1\n"
     99 	"u\t\tNavigate one path element up\n"
    100 	"i\r\t\tShow MIME type parameters\n"
    101 	"H\t\tView all page history\n"
    102 	"m [title]\t\tSave bookmark for current page (uses first header as name if title is omitted)\n"
    103 	"M\t\tBrowse bookmarks\n"
    104 	"K\t\tRemove bookmark for current page\n"
    105 	"r\t\tReload the page\n"
    106 	"s\t\tSearch via geminispace.info\n"
    107 	"l\t\tSearch backlinks to current page via geminispace.info\n"
    108 	"/<text>\t\tSearch for text (POSIX regular expression)\n"
    109 	"n\t\tJump to next search match\n"
    110 	"d[N] [path]\tDownload page, or Nth link, to path\n"
    111 	"[N]|<prog>\tPipe page, or Nth link, into program\n"
    112 	"a\t\tToggle usage of alt text instead of preformatted text\n"
    113 	"q\t\tQuit\n"
    114 	"\n"
    115 	"[N] must be replaced with a number >= 0\n"
    116 	"\n"
    117 	;
    118 
    119 static void
    120 usage(const char *argv_0)
    121 {
    122 	fprintf(stderr, "usage: %s [-PUAT] [-j mode] [-R redirs] [-W width] [gemini://...]\n", argv_0);
    123 }
    124 
    125 static void
    126 history_free(struct history *history)
    127 {
    128 	if (!history) {
    129 		return;
    130 	}
    131 	history_free(history->next);
    132 	free(history->url);
    133 	free(history);
    134 }
    135 
    136 static bool
    137 set_url(struct browser *browser, char *new_url, struct history **history)
    138 {
    139 	if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) {
    140 		fprintf(stderr, "Error: invalid URL\n");
    141 		return false;
    142 	}
    143 	if (browser->plain_url != NULL) {
    144 		free(browser->plain_url);
    145 	}
    146 	curl_url_get(browser->url, CURLUPART_URL, &browser->plain_url, 0);
    147 	if (history) {
    148 		struct history *next = calloc(1, sizeof(struct history));
    149 		curl_url_get(browser->url, CURLUPART_URL, &next->url, 0);
    150 		next->prev = *history;
    151 		if (*history) {
    152 			if ((*history)->next) {
    153 				history_free((*history)->next);
    154 			}
    155 			(*history)->next = next;
    156 		}
    157 		*history = next;
    158 	}
    159 	return true;
    160 }
    161 
    162 static char *
    163 get_data_pathfmt()
    164 {
    165 	const struct pathspec paths[] = {
    166 		{.var = "GMNIDATA", .path = "/%s"},
    167 		{.var = "XDG_DATA_HOME", .path = "/gmni/%s"},
    168 		{.var = "HOME", .path = "/.local/share/gmni/%s"}
    169 	};
    170 	return getpath(paths, sizeof(paths) / sizeof(paths[0]));
    171 }
    172 
    173 static char *
    174 trim_ws(char *in)
    175 {
    176 	while (*in && isspace(*in)) ++in;
    177 	return in;
    178 }
    179 
    180 static void
    181 save_bookmark(struct browser *browser, const char *title)
    182 {
    183 	char *path_fmt = get_data_pathfmt();
    184 	static char path[PATH_MAX+1];
    185 	static char dname[PATH_MAX+1];
    186 	size_t n;
    187 
    188 	n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi");
    189 	free(path_fmt);
    190 	assert(n < sizeof(path));
    191 	posix_dirname(path, dname);
    192 	if (mkdirs(dname, 0755) != 0) {
    193 		fprintf(stderr, "Error creating directory %s: %s\n",
    194 				dname, strerror(errno));
    195 		return;
    196 	}
    197 
    198 	FILE *f = fopen(path, "a");
    199 	if (!f) {
    200 		fprintf(stderr, "Error opening %s for writing: %s\n",
    201 				path, strerror(errno));
    202 		return;
    203 	}
    204 
    205 	fprintf(f, "=> %s%s%s\n", browser->plain_url,
    206 		title ? " " : "", title ? title : "");
    207 	fclose(f);
    208 
    209 	fprintf(browser->tty, "Bookmark saved: %s\n",
    210 		title ? title : browser->plain_url);
    211 }
    212 
    213 static void
    214 remove_bookmark(struct browser *browser)
    215 {
    216 	char *path_fmt = get_data_pathfmt();
    217 	static char path[PATH_MAX+1];
    218 	snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi");
    219 	free(path_fmt);
    220 
    221 	static char tempfile[PATH_MAX+2];
    222 	snprintf(tempfile, sizeof(tempfile), "%s2", path);
    223 	FILE *fi = fopen(path, "r");
    224 	FILE *fo = fopen(tempfile, "w");
    225 	if(fi == NULL) {
    226 		fprintf(stderr, "Bookmark file not available!\n");
    227 		return;
    228 	}
    229 	if(fo == NULL) {
    230 		fprintf(stderr, "tempfile not available!\n");
    231 		return;
    232 	}
    233 	
    234 	char *line = NULL;
    235 	size_t len = 0;
    236 	size_t n = 0;
    237 
    238 	static char url[1024];
    239 	n = snprintf(url, sizeof(url), "=> %s ", browser->plain_url);
    240 	while(getline(&line, &len, fi) != -1) {
    241 		if (strncmp(line, url, n)==0) {
    242 			fprintf(browser->tty, "Bookmark removed!\n");
    243 		} else {
    244 			fprintf(fo, "%s", line);
    245 		}
    246 	}
    247 
    248 	fclose(fi);
    249 	fclose(fo);
    250 	free(line);
    251 	if ( rename(tempfile, path) != 0) {
    252 		fprintf(browser->tty, "Failed to update bookmarks: %s\n", strerror(errno));
    253 	}
    254 }
    255 
    256 static void
    257 open_bookmarks(struct browser *browser)
    258 {
    259 	char *path_fmt = get_data_pathfmt();
    260 	static char path[PATH_MAX+1];
    261 	static char dname[PATH_MAX+1];
    262 	size_t n;
    263 
    264 	n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi");
    265 	free(path_fmt);
    266 
    267 	assert(n < sizeof(path));
    268 	posix_dirname(path, dname);
    269 	if (mkdirs(dname, 0755) != 0) {
    270 		fprintf(stderr, "Error creating directory %s: %s\n",
    271 				dname, strerror(errno));
    272 		return;
    273 	}
    274 
    275 	struct stat buf;
    276 	if (stat(path, &buf) == -1 && errno == ENOENT) {
    277 		// TOCTOU, but we almost certainly don't care
    278 		FILE *f = fopen(path, "a");
    279 		if (f == NULL) {
    280 			fprintf(stderr, "Error opening %s for writing: %s\n",
    281 					path, strerror(errno));
    282 			return;
    283 		}
    284 		fprintf(f, default_bookmarks, path);
    285 		fclose(f);
    286 	}
    287 
    288 	static char url[PATH_MAX+1+7];
    289 	snprintf(url, sizeof(url), "file://%s", path);
    290 	set_url(browser, url, &browser->history);
    291 }
    292 
    293 static void
    294 print_media_parameters(FILE *out, char *params)
    295 {
    296 	if (params == NULL) {
    297 		fprintf(out, "No media parameters\n");
    298 		return;
    299 	}
    300 	for (char *param = strtok(params, ";"); param;
    301 			param = strtok(NULL, ";")) {
    302 		char *value = strchr(param, '=');
    303 		if (value == NULL) {
    304 			fprintf(out, "Invalid media type parameter '%s'\n",
    305 				trim_ws(param));
    306 			continue;
    307 		}
    308 		*value = 0;
    309 		fprintf(out, "%s: ", trim_ws(param));
    310 		*value++ = '=';
    311 		if (*value != '"') {
    312 			fprintf(out, "%s\n", value);
    313 			continue;
    314 		}
    315 		while (value++) {
    316 			switch (*value) {
    317 			case '\0':
    318 				if ((value = strtok(NULL, ";")) != NULL) {
    319 					fprintf(out, ";%c", *value);
    320 				}
    321 				break;
    322 			case '"':
    323 				value = NULL;
    324 				break;
    325 			case '\\':
    326 				if (value[1] == '\0') {
    327 					break;
    328 				}
    329 				value++;
    330 				/* fallthrough */
    331 			default:
    332 				putc(*value, out);
    333 			}
    334 		}
    335 		putc('\n', out);
    336 	}
    337 }
    338 
    339 static char *
    340 get_input(const struct gemini_response *resp, FILE *source)
    341 {
    342 	int r = 0;
    343 	struct termios attrs;
    344 	bool tty = fileno(source) != -1 && isatty(fileno(source));
    345 	char *input = NULL;
    346 	if (tty) {
    347 		fprintf(stderr, "%s: ", resp->meta);
    348 		if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) {
    349 			r = tcgetattr(fileno(source), &attrs);
    350 			struct termios new_attrs;
    351 			r = tcgetattr(fileno(source), &new_attrs);
    352 			if (r != -1) {
    353 				new_attrs.c_lflag &= ~ECHO;
    354 				tcsetattr(fileno(source), TCSANOW, &new_attrs);
    355 			}
    356 		}
    357 	}
    358 	size_t s = 0;
    359 	ssize_t n = getline(&input, &s, source);
    360 	if (n == -1) {
    361 		fprintf(stderr, "Error reading input: %s\n",
    362 			feof(source) ? "EOF" : strerror(ferror(source)));
    363 		return NULL;
    364 	}
    365 	input[n - 1] = '\0'; // Drop LF
    366 	if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) {
    367 		tcsetattr(fileno(source), TCSANOW, &attrs);
    368 	}
    369 	return input;
    370 }
    371 
    372 static bool
    373 has_suffix(char *str, char *suff)
    374 {
    375 	size_t suffl = strlen(suff);
    376 	size_t strl = strlen(str);
    377 	if (strl < suffl) {
    378 		return false;
    379 	}
    380 	return strcmp(&str[strl - suffl], suff) == 0;
    381 }
    382 
    383 static void
    384 pipe_resp(FILE *out, struct gemini_response resp, char *cmd) {
    385 	char buf[BUFSIZ];
    386 	int pfd[2];
    387 	if (pipe(pfd) == -1) {
    388 		perror("pipe");
    389 		return;
    390 	}
    391 	pid_t pid;
    392 	switch ((pid = fork())) {
    393 	case -1:
    394 		perror("fork");
    395 		return;
    396 	case 0:
    397 		close(pfd[1]);
    398 		dup2(pfd[0], STDIN_FILENO);
    399 		close(pfd[0]);
    400 		execlp("sh", "sh", "-c", cmd, NULL);
    401 		perror("exec");
    402 		_exit(1);
    403 	}
    404 	close(pfd[0]);
    405 	FILE *f = fdopen(pfd[1], "w");
    406 	// XXX: may affect history, do we care?
    407 	for (int n = 1; n > 0;) {
    408 		if (resp.sc) {
    409 			n = br_sslio_read(&resp.body, buf, BUFSIZ);
    410 		} else {
    411 			n = read(resp.fd, buf, BUFSIZ);
    412 		}
    413 		if (n < 0) {
    414 			n = 0;
    415 		}
    416 		ssize_t w = 0;
    417 		while (w < (ssize_t)n) {
    418 			ssize_t x = fwrite(&buf[w], 1, n - w, f);
    419 			if (ferror(f)) {
    420 				fprintf(stderr, "Error: write: %s\n",
    421 					strerror(errno));
    422 				return;
    423 			}
    424 			w += x;
    425 		}
    426 	}
    427 	fclose(f);
    428 	int status;
    429 	waitpid(pid, &status, 0);
    430 	if (status != 0) {
    431 		fprintf(out, "Command exited %d\n", status);
    432 	}
    433 }
    434 
    435 static enum gemini_result
    436 do_requests(struct browser *browser, struct gemini_response *resp)
    437 {
    438 	int nredir = 0;
    439 	bool requesting = true;
    440 	enum gemini_result res;
    441 
    442 	char *scheme;
    443 	CURLUcode uc = curl_url_get(browser->url,
    444 		CURLUPART_SCHEME, &scheme, 0);
    445 	assert(uc == CURLUE_OK); // Invariant
    446 
    447 	char *host = NULL;
    448 	struct gmni_client_certificate client_cert = {0};
    449 	const struct pathspec paths[] = {
    450 		{.var = "GMNIDATA", .path = "/certs/%s.%s"},
    451 		{.var = "XDG_DATA_HOME", .path = "/gmni/certs/%s.%s"},
    452 		{.var = "HOME", .path = "/.local/share/gmni/certs/%s.%s"}
    453 	};
    454 	char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0]));
    455 	char certpath[PATH_MAX+1], keypath[PATH_MAX+1];
    456 	size_t n = 0;
    457 
    458 	if (strcmp(scheme, "gemini") == 0) {
    459 		CURLUcode uc = curl_url_get(browser->url,
    460 			CURLUPART_HOST, &host, 0);
    461 		assert(uc == CURLUE_OK);
    462 
    463 		n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt");
    464 		assert(n < sizeof(certpath));
    465 		FILE *certin = fopen(certpath, "r");
    466 		if (certin) {
    467 			n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key");
    468 			assert(n < sizeof(keypath));
    469 
    470 			FILE *skin = fopen(keypath, "r");
    471 			if (gmni_ccert_load(&client_cert, certin, skin)) {
    472 				browser->opts.client_cert = NULL;
    473 				fprintf(stderr, "Unable to load client certificate for host %s", host);
    474 			} else {
    475 				browser->opts.client_cert = &client_cert;
    476 			}
    477 		} else {
    478 			browser->opts.client_cert = NULL;
    479 		}
    480 	}
    481 
    482 	while (requesting) {
    483 		if (strcmp(scheme, "file") == 0) {
    484 			requesting = false;
    485 
    486 			char *path;
    487 			uc = curl_url_get(browser->url,
    488 				CURLUPART_PATH, &path, 0);
    489 			if (uc != CURLUE_OK) {
    490 				resp->status = GEMINI_STATUS_BAD_REQUEST;
    491 				break;
    492 			}
    493 
    494 			int fd = open(path, O_RDONLY);
    495 			if (fd < 0) {
    496 				resp->status = GEMINI_STATUS_NOT_FOUND;
    497 				// Make sure members of resp evaluate to false,
    498 				// so that gemini_response_finish does not try
    499 				// to free them.
    500 				resp->sc = NULL;
    501 				resp->meta = NULL;
    502 				resp->fd = -1;
    503 				free(path);
    504 				break;
    505 			}
    506 
    507 			if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) {
    508 				resp->meta = strdup("text/gemini");
    509 			} else if (has_suffix(path, ".txt")) {
    510 				resp->meta = strdup("text/plain");
    511 			} else {
    512 				resp->meta = strdup("application/x-octet-stream");
    513 			}
    514 			free(path);
    515 			resp->status = GEMINI_STATUS_SUCCESS;
    516 			resp->fd = fd;
    517 			resp->sc = NULL;
    518 			res = GEMINI_OK;
    519 			goto out;
    520 		}
    521 
    522 		res = gemini_request(browser->plain_url, &browser->opts,
    523 				&browser->tofu, resp);
    524 		if (res != GEMINI_OK) {
    525 			fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp));
    526 			requesting = false;
    527 			resp->status = 70 + res;
    528 			break;
    529 		}
    530 
    531 		char *input;
    532 		switch (gemini_response_class(resp->status)) {
    533 		case GEMINI_STATUS_CLASS_INPUT:
    534 			input = get_input(resp, browser->tty);
    535 			if (!input) {
    536 				requesting = false;
    537 				break;
    538 			}
    539 			if (input[0] == '\0' && browser->history->prev) {
    540 				free(input);
    541 				browser->history = browser->history->prev;
    542 				set_url(browser, browser->history->url, NULL);
    543 				break;
    544 			}
    545 
    546 			char *new_url = gemini_input_url(
    547 				browser->plain_url, input);
    548 			free(input);
    549 			assert(new_url);
    550 			set_url(browser, new_url,
    551 				resp->status == GEMINI_STATUS_SENSITIVE_INPUT ?
    552 				NULL : &browser->history);
    553 			free(new_url);
    554 			break;
    555 		case GEMINI_STATUS_CLASS_REDIRECT:
    556 			if (browser->max_redirs == REDIRS_ASK) {
    557 again:
    558 				fprintf(browser->tty,
    559 					"The host %s is redirecting to:\n"
    560 					"%s\n\n"
    561 					"[f]ollow redirect; [a]bort\n"
    562 					"=> ", host, resp->meta);
    563 
    564 				size_t sz = 0;
    565 				char *line = NULL;
    566 				if (getline(&line, &sz, browser->tty) == -1) {
    567 					free(line);
    568 					requesting = false;
    569 					break;
    570 				}
    571 				if (line[1] != '\n') {
    572 					free(line);
    573 					goto again;
    574 				}
    575 
    576 				char c = line[0];
    577 				free(line);
    578 
    579 				if (c == 'a') {
    580 					requesting = false;
    581 					break;
    582 				} else if (c != 'f') {
    583 					goto again;
    584 				}
    585 			} else if (browser->max_redirs != REDIRS_UNLIMITED
    586 					&& ++nredir >= browser->max_redirs) {
    587 				requesting = false;
    588 				fprintf(stderr, "Error: maximum redirects (%d) exceeded\n",
    589 					browser->max_redirs);
    590 				break;
    591 			}
    592 			set_url(browser, resp->meta, NULL);
    593 			break;
    594 		case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED:
    595 			requesting = false;
    596 			assert(host);
    597 			n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt");
    598 			assert(n < sizeof(certpath));
    599 			n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key");
    600 			char dname[PATH_MAX + 1];
    601 			posix_dirname(certpath, dname);
    602 			if (mkdirs(dname, 0755) != 0) {
    603 				fprintf(stderr, "Error creating directory %s: %s\n",
    604 						dname, strerror(errno));
    605 				break;
    606 			}
    607 			assert(n < sizeof(keypath));
    608 			fprintf(stderr, "The server requested a client certificate.\n"
    609 				"Presently, this process is not automated.\n"
    610 				"The following OpenSSL command will generate a certificate for this host:\n\n"
    611 				"openssl req -x509 -newkey rsa:4096 \\\n\t-keyout %s \\\n\t-out %s \\\n\t-days 36500 -nodes\n\n"
    612 				"Use the 'r' command to reload the page after creating this certificate.\n",
    613 				keypath, certpath);
    614 			break;
    615 		case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE:
    616 		case GEMINI_STATUS_CLASS_PERMANENT_FAILURE:
    617 			requesting = false;
    618 			fprintf(stderr, "Server returned %s %d %s\n",
    619 				resp->status / 10 == 4 ?
    620 				"TEMPORARY FAILURE" : "PERMANENT FAILURE",
    621 				resp->status, resp->meta);
    622 			break;
    623 		case GEMINI_STATUS_CLASS_SUCCESS:
    624 			goto out;
    625 		}
    626 
    627 		if (requesting) {
    628 			gemini_response_finish(resp);
    629 		}
    630 	}
    631 
    632 out:
    633 	if (client_cert.key) {
    634 		free(client_cert.key);
    635 	}
    636 	free(path_fmt);
    637 	free(scheme);
    638 	free(host);
    639 	return res;
    640 }
    641 
    642 static enum prompt_result
    643 do_prompts(const char *prompt, struct browser *browser)
    644 {
    645 	enum prompt_result result = PROMPT_AGAIN;
    646 	fprintf(browser->tty, "%s", prompt);
    647 
    648 	size_t l = 0;
    649 	
    650 	char curr_url[1024] = {0};
    651 	char save_url[1024] = {0};
    652 	
    653 	char *in = NULL;
    654 	ssize_t n = getline(&in, &l, browser->tty);
    655 	if (n == -1 && feof(browser->tty)) {
    656 		fputc('\n', browser->tty);
    657 		result = PROMPT_QUIT;
    658 		goto exit;
    659 	}
    660 
    661 	in[n - 1] = 0; // Remove LF
    662 	char *endptr;
    663 
    664 	CURLU *url = curl_url();
    665 	bool isurl = curl_url_set(url, CURLUPART_URL, in, 0) == CURLUE_OK;
    666 	curl_url_cleanup(url);
    667 	if (isurl) {
    668 		set_url(browser, in, &browser->history);
    669 		result = PROMPT_ANSWERED;
    670 		goto exit;
    671 	}
    672 
    673 	int historyhops = 1;
    674 	int r;
    675 	switch (in[0]) {
    676 	case '\0':
    677 		result = PROMPT_MORE;
    678 		goto exit;
    679 	case '.':
    680 		break;
    681 	case 'q':
    682 		if (in[1]) {
    683 			fprintf(stderr, "Error: unrecognized command.\n");
    684 			goto exit;
    685 		}
    686 		result = PROMPT_QUIT;
    687 		goto exit;
    688 	case 'b':
    689 		if (in[1] && isdigit(in[1])) {
    690 			historyhops =(int)strtol(in+1, &endptr, 10);
    691 			if (endptr[0]) {
    692 				fprintf(stderr, "Error: invalid argument.\n");
    693 				goto exit;
    694 			}
    695 		} else if (in[1]) {
    696 			fprintf(stderr, "Error: invalid argument.\n");
    697 			goto exit;
    698 		}
    699 		while (historyhops > 0) {
    700 			if (browser->history->prev) {
    701 				browser->history = browser->history->prev;
    702 			}
    703 			historyhops--;
    704 		}
    705 		set_url(browser, browser->history->url, NULL);
    706 		result = PROMPT_ANSWERED;
    707 		goto exit;
    708 	case 's':
    709 		if (in[1]) break;
    710 		set_url(browser, "gemini://geminispace.info/search", &browser->history);
    711 		result = PROMPT_ANSWERED;
    712 		goto exit;
    713 	case 'a':
    714 		browser->alttext = !browser->alttext;
    715 		fprintf(browser->tty, "Alttext instead of preformatted block is now %s\n\n", browser->alttext ? "ENABLED" : "DISABLED");
    716 		result = PROMPT_AGAIN;
    717 		goto exit;
    718 	case 'l':
    719 		snprintf(curr_url, sizeof(curr_url), "gemini://geminispace.info/backlinks?%s", browser->plain_url);
    720 		set_url(browser, curr_url, &browser->history);
    721 		result = PROMPT_ANSWERED;
    722 		goto exit;
    723 	case 'f':
    724 		if (in[1] && isdigit(in[1])) {
    725 			historyhops =(int)strtol(in+1, &endptr, 10);
    726 			if (endptr[0]) {
    727 				fprintf(stderr, "Error: invalid argument.\n");
    728 				goto exit;
    729 			}
    730 		} else if (in[1]) {
    731 			fprintf(stderr, "Error: invalid argument.\n");
    732 			goto exit;
    733 		}
    734 		while (historyhops > 0) {
    735 			if (browser->history->next) {
    736 				browser->history = browser->history->next;
    737 			}
    738 			historyhops--;
    739 		}
    740 		set_url(browser, browser->history->url, NULL);
    741 		result = PROMPT_ANSWERED;
    742 		goto exit;
    743 	case 'u':;
    744 		int keep = 0;
    745 		int len = strlen(browser->plain_url);
    746 		for (int i=0; i<len; i++)
    747 		{
    748 			// ignore trailing / on uri path
    749 			if (browser->plain_url[i] == '/' && i != len-1) {
    750 				keep = i;
    751 			}   
    752 		}
    753 		if (keep > 9) {
    754 			strncpy(curr_url , browser->plain_url, keep+1);
    755 			set_url(browser, curr_url, &browser->history);
    756 		}
    757 		result = PROMPT_ANSWERED;
    758 		goto exit;
    759 	case 'H':
    760 		if (in[1]) {
    761 			fprintf(stderr, "Error: unrecognized command.\n");
    762 			goto exit;
    763 		}
    764 		struct history *cur = browser->history;
    765 		int hist_count = 0;
    766 		while (cur->prev) {
    767 			cur = cur->prev;
    768 			hist_count++;
    769 		}
    770 		while (cur != browser->history) {
    771 			fprintf(browser->tty, "b%-3i %s\n", hist_count--, cur->url);
    772 			cur = cur->next;
    773 		}
    774 		fprintf(browser->tty, "*    %s\n", cur->url);
    775 		cur = cur->next;
    776 		while (cur) {
    777 			fprintf(browser->tty, "f%-3i %s\n", ++hist_count, cur->url);
    778 			cur = cur->next;
    779 		}
    780 		goto exit;
    781 	case 'm':
    782 		if (in[1] != '\0' && !isspace(in[1])) {
    783 			fprintf(stderr, "Error: unrecognized command.\n");
    784 			goto exit;
    785 		}
    786 		char *title = in[1] ? &in[1] : browser->page_title;
    787 		save_bookmark(browser, title ? trim_ws(title) : title);
    788 		goto exit;
    789 	case 'M':
    790 		if (in[1]) {
    791 			fprintf(stderr, "Error: unrecognized command.\n");
    792 			goto exit;
    793 		}
    794 		open_bookmarks(browser);
    795 		result = PROMPT_ANSWERED;
    796 		goto exit;
    797 	case 'K':
    798 		if (in[1]) break;
    799 		remove_bookmark(browser);
    800 		result = PROMPT_AGAIN;
    801 		goto exit;
    802 	case 'e':
    803 	case 't':
    804 		strncpy(&save_url[0], browser->plain_url, sizeof(save_url)-1);
    805 		if (!in[1]) {
    806 			strncpy(&curr_url[0], browser->plain_url, sizeof(curr_url)-1);
    807 		} else {
    808 			struct link *link = browser->links;
    809 			int linksel = (int)strtol(in+1, &endptr, 10);
    810 			if (!endptr[0] && linksel >= 0) {
    811 				while (linksel > 0 && link) {
    812 					link = link->next;
    813 					--linksel;
    814 				}
    815 				if (!link) {
    816 					fprintf(stderr, "Error: no such link.\n");
    817 				} else {
    818 					strncpy(&curr_url[0], link->url, sizeof(curr_url)-1);
    819 				}
    820 			}
    821 		}
    822 		if (curr_url[0]) {
    823 			fprintf(browser->tty, "=> %s\n", curr_url);
    824 			char *tempfile;
    825 			tempfile = tmpnam(NULL);
    826 			if (in[0] == 't') {
    827 				struct gemini_response resp;
    828 				set_url(browser, curr_url, NULL);
    829 				enum gemini_result res = do_requests(browser, &resp);
    830 				if (res != GEMINI_OK) {
    831 					fprintf(stderr, "Error: %s\n",
    832 					gemini_strerr(res, &resp));
    833 				} else {
    834 					download_resp(browser->tty, resp, tempfile, curr_url);
    835 				}
    836 				gemini_response_finish(&resp);
    837 				set_url(browser, save_url, NULL);
    838 			}
    839 			if (in[0] == 'e' || browser->autoopen) {				
    840 				char target[1050];
    841 				snprintf(target, sizeof(target), "xdg-open %s >/dev/null 2>&1", in[0] == 't' ? tempfile : curr_url);
    842 				if ( !system(target) ) fprintf(browser->tty, "Link send to xdg-open\n");
    843 			}
    844 			fprintf(browser->tty, "\n");
    845 		}
    846 		result = PROMPT_AGAIN;
    847 		goto exit;
    848 	case '/':
    849 		if (!in[1]) break;
    850 		if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) {
    851 			static char buf[1024];
    852 			r = regerror(r, &browser->regex, buf, sizeof(buf));
    853 			assert(r < (int)sizeof(buf));
    854 			fprintf(stderr, "Error: %s\n", buf);
    855 		} else {
    856 			browser->searching = true;
    857 			result = PROMPT_ANSWERED;
    858 		}
    859 		goto exit_re;
    860 	case 'n':
    861 		if (in[1]) {
    862 			fprintf(stderr, "Error: unrecognized command.\n");
    863 			goto exit;
    864 		}
    865 		if (browser->searching) {
    866 			result = PROMPT_NEXT;
    867 			goto exit_re;
    868 		} else {
    869 			fprintf(stderr, "Cannot move to next result; we are not searching for anything\n");
    870 			goto exit;
    871 		}
    872 	case 'p':
    873 		if (!in[1]) {
    874 			fprintf(stderr, "Error: missing argument.\n");
    875 			goto exit;
    876 		} else if (!isdigit(in[1])) {
    877 			fprintf(stderr, "Error: invalid argument.\n");
    878 			goto exit;
    879 		}
    880 		struct link *link = browser->links;
    881 		int linksel = (int)strtol(in+1, &endptr, 10);
    882 		if (!endptr[0] && linksel >= 0) {
    883 			while (linksel > 0 && link) {
    884 				link = link->next;
    885 				--linksel;
    886 			}
    887 
    888 			if (!link) {
    889 				fprintf(stderr, "Error: no such link.\n");
    890 			} else {
    891 				fprintf(browser->tty, "=> %s\n", link->url);
    892 				goto exit;
    893 			}
    894 		} else {
    895 			fprintf(stderr, "Error: invalid argument.\n");
    896 		}
    897 		goto exit;
    898 	case 'r':
    899 		if (in[1]) {
    900 			fprintf(stderr, "Error: unrecognized command.\n");
    901 			goto exit;
    902 		}
    903 		result = PROMPT_ANSWERED;
    904 		goto exit;
    905 	case 'i':
    906 		if (in[1]) {
    907 			fprintf(stderr, "Error: unrecognized command.\n");
    908 			goto exit;
    909 		}
    910 		print_media_parameters(browser->tty, browser->meta
    911 				? strchr(browser->meta, ';') : NULL);
    912 		goto exit;
    913 	case 'd':
    914 		endptr = &in[1];
    915 		char *d_url = browser->plain_url;
    916 		if (in[1] != '\0' && !isspace(in[1])) {
    917 			struct link *link = browser->links;
    918 			int linksel = (int)strtol(in+1, &endptr, 10);
    919 			while (linksel > 0 && link) {
    920 				link = link->next;
    921 				--linksel;
    922 			}
    923 
    924 			if (!link) {
    925 				fprintf(stderr, "Error: no such link.\n");
    926 				goto exit;
    927 			} else {
    928 				d_url = link->url;
    929 			}
    930 		}
    931 		struct gemini_response resp;
    932 		strncpy(&save_url[0], browser->plain_url, sizeof(url)-1);
    933 		strncpy(&curr_url[0], d_url, sizeof(url)-1);
    934 		// XXX: may affect history, do we care?
    935 		set_url(browser, curr_url, NULL);
    936 		enum gemini_result res = do_requests(browser, &resp);
    937 		if (res != GEMINI_OK) {
    938 			fprintf(stderr, "Error: %s\n",
    939 				gemini_strerr(res, &resp));
    940 			set_url(browser, save_url, NULL);
    941 			goto exit;
    942 		}
    943 		download_resp(browser->tty, resp, trim_ws(endptr), curr_url);
    944 		gemini_response_finish(&resp);
    945 		set_url(browser, save_url, NULL);
    946 		goto exit;
    947 	case '|':
    948 		strncpy(&curr_url[0], browser->plain_url, sizeof(url)-1);
    949 		res = do_requests(browser, &resp);
    950 		if (res != GEMINI_OK) {
    951 			fprintf(stderr, "Error: %s\n",
    952 				gemini_strerr(res, &resp));
    953 			goto exit;
    954 		}
    955 		pipe_resp(browser->tty, resp, &in[1]);
    956 		gemini_response_finish(&resp);
    957 		set_url(browser, curr_url, NULL);
    958 		goto exit;
    959 	case '?':
    960 		if (in[1]) {
    961 			fprintf(stderr, "Error: unrecognized command.\n");
    962 			goto exit;
    963 		}
    964 		fprintf(browser->tty, "%s", help_msg);
    965 		goto exit;
    966 	default:
    967 		if (isdigit(in[0])) break;
    968 		fprintf(stderr, "Error: unrecognized command.\n");
    969 		goto exit;
    970 	}
    971 
    972 	if (isdigit(in[0])) {
    973 		struct link *link = browser->links;
    974 		int linksel = (int)strtol(in, &endptr, 10);
    975 		if ((endptr[0] && endptr[0] != '|') || linksel < 0) {
    976 			fprintf(stderr, "Error: no such link.\n");
    977 			goto exit;
    978 		}
    979 
    980 		while (linksel > 0 && link) {
    981 			link = link->next;
    982 			--linksel;
    983 		}
    984 
    985 		if (!link) {
    986 			fprintf(stderr, "Error: no such link.\n");
    987 			goto exit;
    988 		} else if (endptr[0] == '|') {
    989 			struct gemini_response resp;
    990 			strncpy(curr_url, browser->plain_url, sizeof(curr_url) - 1);
    991 			set_url(browser, link->url, &browser->history);
    992 			enum gemini_result res = do_requests(browser, &resp);
    993 			if (res != GEMINI_OK) {
    994 				fprintf(stderr, "Error: %s\n",
    995 					gemini_strerr(res, &resp));
    996 				set_url(browser, curr_url, NULL);
    997 				goto exit;
    998 			}
    999 			pipe_resp(browser->tty, resp, &endptr[1]);
   1000 			gemini_response_finish(&resp);
   1001 			set_url(browser, curr_url, NULL);
   1002 			goto exit;
   1003 		} else {
   1004 			assert(endptr[0] == '\0');
   1005 			set_url(browser, link->url, &browser->history);
   1006 			result = PROMPT_ANSWERED;
   1007 			goto exit;
   1008 		}
   1009 	}
   1010 
   1011 	set_url(browser, in, &browser->history);
   1012 	result = PROMPT_ANSWERED;
   1013 exit:
   1014 	if (browser->searching) {
   1015 		browser->searching = false;
   1016 		regfree(&browser->regex);
   1017 	}
   1018 exit_re:
   1019 	free(in);
   1020 	return result;
   1021 }
   1022 
   1023 static int
   1024 wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col)
   1025 {
   1026 	if (!s[0]) {
   1027 		fprintf(f, "\n");
   1028 		return 0;
   1029 	}
   1030 	for (int i = 0; s[i]; ++i) {
   1031 		switch (s[i]) {
   1032 		case '\n':
   1033 			assert(0); // Not supposed to happen
   1034 		case '\t':
   1035 			*col = *col + (8 - *col % 8);
   1036 			break;
   1037 		case '\r':
   1038 			if (!s[i+1]) break;
   1039 			/* fallthrough */
   1040 		default:
   1041 			// skip unicode continuation bytes
   1042 			if ((s[i] & 0xc0) == 0x80) break;
   1043 
   1044 			if (iscntrl(s[i])) s[i] = '.';
   1045 			*col += 1;
   1046 			break;
   1047 		}
   1048 
   1049 		if (*col >= ws->ws_col - 4) {
   1050 			int j = i--;
   1051 			while (&s[i] != s && !isspace(s[i])) --i;
   1052 			if (&s[i] == s) i = j;
   1053 			char c = s[i];
   1054 			s[i] = 0;
   1055 			int n = fprintf(f, "%s\n", s) - (isspace(c) ? 0 : 1);
   1056 			s[i] = c;
   1057 			*row += 1;
   1058 			*col = 0;
   1059 			return n;
   1060 		}
   1061 	}
   1062 	return fprintf(f, "%s\n", s) - 1;
   1063 }
   1064 
   1065 static int
   1066 resp_read(void *state, void *buf, size_t nbyte)
   1067 {
   1068 	struct gemini_response *resp = state;
   1069 	if (resp->sc) {
   1070 		return br_sslio_read(&resp->body, buf, nbyte);
   1071 	} else {
   1072 		return read(resp->fd, buf, nbyte);
   1073 	}
   1074 }
   1075 
   1076 static bool
   1077 display_gemini(struct browser *browser, struct gemini_response *resp)
   1078 {
   1079 	int nlinks = 0;
   1080 	struct gemini_parser p;
   1081 	gemini_parser_init(&p, &resp_read, resp);
   1082 	free(browser->page_title);
   1083 	browser->page_title = NULL;
   1084 
   1085 	FILE *out = browser->tty;
   1086 	bool searching = browser->searching;
   1087 	if (searching) {
   1088 		out = fopen("/dev/null", "w+");
   1089 	}
   1090 	bool alttext_printed = false;
   1091 
   1092 	fprintf(out, "\n");
   1093 	char *text = NULL;
   1094 	int row = 0, col = 0;
   1095 	struct gemini_token tok;
   1096 	struct link **next = &browser->links;
   1097 
   1098 	char prompt[4096];
   1099 	int info_rows = 0;
   1100 	struct winsize ws;
   1101 	bool first_screen = 1;
   1102 	while (text != NULL || gemini_parser_next(&p, &tok) == 0) {
   1103 repeat:
   1104 		if (!row) {
   1105 			ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);
   1106 			if (browser->max_width != 0 && ws.ws_col > browser->max_width) {
   1107 				ws.ws_col = browser->max_width;
   1108 			}
   1109 
   1110 			char *end = NULL;
   1111 			if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) {
   1112 				*end = 0;
   1113 			}
   1114 			snprintf(prompt, sizeof(prompt), "\n%s at %s\n"
   1115 				"[Enter]: read more; %s[N]: =follow Nth link; %s%s[q]uit; [?]; or type a URL\n"
   1116 				"(more) => ", resp->meta, browser->plain_url,
   1117 				browser->searching ? "[n]ext result; " : "",
   1118 				browser->history->prev ? "[b]ack; " : "",
   1119 				browser->history->next ? "[f]orward; " : "");
   1120 			if (end != NULL) {
   1121 				*end = ';';
   1122 			}
   1123 
   1124 			info_rows = 0;
   1125 			for (char *ln = prompt; (end = strchr(ln, '\n')); ln = end + 1) {
   1126 				*end = '\0';
   1127 				int len = strlen(ln);
   1128 				info_rows += len / ws.ws_col + (len % ws.ws_col != 0);
   1129 				if (!*ln) info_rows++; // empty line
   1130 				*end = '\n';
   1131 			}
   1132 			// if not first screen, text is preceded by an empty line,
   1133 			// and help text is followed by a prompt line
   1134 			info_rows += !first_screen + 1;
   1135 		}
   1136 
   1137 		switch (tok.token) {
   1138 		case GEMINI_TEXT:
   1139 			col += fprintf(out, "     ");
   1140 			if (text == NULL) {
   1141 				text = tok.text;
   1142 			}
   1143 			break;
   1144 		case GEMINI_LINK:
   1145 			if (text == NULL) {
   1146 				col += fprintf(out, "%3d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_LMAGENTA));
   1147 				text = trim_ws(tok.link.text ? tok.link.text : tok.link.url);
   1148 				*next = calloc(1, sizeof(struct link));
   1149 				(*next)->url = strdup(trim_ws(tok.link.url));
   1150 				next = &(*next)->next;
   1151 			} else {
   1152 				col += fprintf(out, "     ");
   1153 			}
   1154 			break;
   1155 		case GEMINI_PREFORMATTED_BEGIN:
   1156 			alttext_printed = false;
   1157 			if (text == NULL && browser->alttext && tok.preformatted != NULL) {
   1158 				fprintf(out, "   A %s", ANSI_COLOR_GRAY);
   1159 				text = trim_ws(tok.preformatted);
   1160 				alttext_printed = true;
   1161 			}
   1162 			break;
   1163 			/* fallthrough */
   1164 		case GEMINI_PREFORMATTED_END:
   1165 			continue; // Not used
   1166 		case GEMINI_PREFORMATTED_TEXT:
   1167 			if (alttext_printed) continue;
   1168 			if (text == NULL) {
   1169 				fprintf(out, "   P %s", ANSI_COLOR_GRAY);
   1170 				text = tok.preformatted;
   1171 			}
   1172 			break;
   1173 		case GEMINI_HEADING:
   1174 			if (!browser->page_title) {
   1175 				browser->page_title = strdup(tok.heading.title);
   1176 			}
   1177 			if (text == NULL) {
   1178 				switch (tok.heading.level) {
   1179 				case 1:
   1180 					col += fprintf(out, "   # %s", ANSI_COLOR_RED);
   1181 					break;
   1182 				case 2:
   1183 					col += fprintf(out, "  ## %s", ANSI_COLOR_YELLOW);
   1184 					break;
   1185 				case 3:
   1186 					col += fprintf(out, " ### %s", ANSI_COLOR_GREEN);
   1187 					break;
   1188 				}
   1189 				text = trim_ws(tok.heading.title);
   1190 			} else {
   1191 				col += fprintf(out, "     ");
   1192 			}
   1193 			break;
   1194 		case GEMINI_LIST_ITEM:
   1195 			if (text == NULL) {
   1196 				col += fprintf(out, "   %s ",
   1197 					browser->unicode ? "•" : "*");
   1198 				text = trim_ws(tok.list_item);
   1199 			} else {
   1200 				col += fprintf(out, "     ");
   1201 			}
   1202 			break;
   1203 		case GEMINI_QUOTE:
   1204 			col += fprintf(out, "   %s ", browser->unicode ? "┃" : ">");
   1205 			if (text == NULL) {
   1206 				text = trim_ws(tok.quote_text);
   1207 			}
   1208 			break;
   1209 		}
   1210 
   1211 		if (text && searching) {
   1212 			int r = regexec(&browser->regex, text, 0, NULL, 0);
   1213 			if (r != 0) {
   1214 				text = NULL;
   1215 				continue;
   1216 			} else {
   1217 				fclose(out);
   1218 				row = col = 0;
   1219 				out = browser->tty;
   1220 				text = NULL;
   1221 				searching = false;
   1222 				goto repeat;
   1223 			}
   1224 		}
   1225 
   1226 		if (text) {
   1227 			int w = wrap(out, text, &ws, &row, &col);
   1228 			text += w;
   1229 			if (text[0] && row < ws.ws_row - info_rows) {
   1230 				continue;
   1231 			}
   1232 
   1233 			if (!text[0]) {
   1234 				text = NULL;
   1235 			}
   1236 		}
   1237 		if (text == NULL) {
   1238 			gemini_token_finish(&tok);
   1239 		}
   1240 
   1241 		while (col >= ws.ws_col) {
   1242 			col -= ws.ws_col;
   1243 			++row;
   1244 		}
   1245 		fprintf(out, ANSI_COLOR_RESET);
   1246 		++row; col = 0;
   1247 
   1248 		if (browser->pagination && row >= ws.ws_row - info_rows) {
   1249 			first_screen = 0;
   1250 			enum prompt_result result = PROMPT_AGAIN;
   1251 			while (result == PROMPT_AGAIN) {
   1252 				result = do_prompts(prompt, browser);
   1253 			}
   1254 
   1255 			switch (result) {
   1256 			case PROMPT_AGAIN:
   1257 			case PROMPT_MORE:
   1258 				printf("\n");
   1259 				break;
   1260 			case PROMPT_QUIT:
   1261 				browser->running = false;
   1262 				if (text != NULL) {
   1263 					gemini_token_finish(&tok);
   1264 				}
   1265 				gemini_parser_finish(&p);
   1266 				return true;
   1267 			case PROMPT_ANSWERED:
   1268 				if (text != NULL) {
   1269 					gemini_token_finish(&tok);
   1270 				}
   1271 				gemini_parser_finish(&p);
   1272 				return true;
   1273 			case PROMPT_NEXT:
   1274 				searching = true;
   1275 				out = fopen("/dev/null", "w");
   1276 				break;
   1277 			}
   1278 
   1279 			row = col = 0;
   1280 		}
   1281 	}
   1282 
   1283 	gemini_token_finish(&tok);
   1284 	gemini_parser_finish(&p);
   1285 	return false;
   1286 }
   1287 
   1288 static bool
   1289 display_plaintext(struct browser *browser, struct gemini_response *resp)
   1290 {
   1291 	struct winsize ws;
   1292 	int row = 0, col = 0;
   1293 	ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);
   1294 
   1295 	char buf[BUFSIZ];
   1296 	for (int n = 1; n > 0;) {
   1297 		if (resp->sc) {
   1298 			n = br_sslio_read(&resp->body, buf, BUFSIZ);
   1299 		} else {
   1300 			n = read(resp->fd, buf, BUFSIZ);
   1301 		}
   1302 		if (n < 0) {
   1303 			n = 0;
   1304 		}
   1305 		for (int i = 0; i < n; i++) {
   1306 			if (iscntrl(buf[i]) && (buf[i] < '\t' || buf[i] > '\v')) {
   1307 				buf[i] = '.';
   1308 			}
   1309 		}
   1310 		ssize_t w = 0;
   1311 		while (w < (ssize_t)n) {
   1312 			ssize_t x = fwrite(&buf[w], 1, n - w, browser->tty);
   1313 			if (ferror(browser->tty)) {
   1314 				fprintf(stderr, "Error: write: %s\n",
   1315 					strerror(errno));
   1316 				return 1;
   1317 			}
   1318 			w += x;
   1319 		}
   1320 	}
   1321 
   1322 	(void)row; (void)col; // TODO: generalize pagination
   1323 	return false;
   1324 }
   1325 
   1326 static bool
   1327 display_response(struct browser *browser, struct gemini_response *resp)
   1328 {
   1329 	if (gemini_response_class(resp->status) != GEMINI_STATUS_CLASS_SUCCESS) {
   1330 		return false;
   1331 	}
   1332 	printf("%c]0;%s%s%c", '\033', browser->plain_url, " - cgmnlm", '\007');
   1333 	if (strcmp(resp->meta, "text/gemini") == 0
   1334 			|| strncmp(resp->meta, "text/gemini;", 12) == 0) {
   1335 		return display_gemini(browser, resp);
   1336 	}
   1337 	if (strncmp(resp->meta, "text/", 5) == 0) {
   1338 		return display_plaintext(browser, resp);
   1339 	}
   1340 	fprintf(stderr, "Media type %s is unsupported, use \"d [path]\" to download this page\n",
   1341 		resp->meta);
   1342 	return false;
   1343 }
   1344 
   1345 static enum tofu_action
   1346 tofu_callback(enum tofu_error error, const char *fingerprint,
   1347 	struct known_host *khost, void *data)
   1348 {
   1349 	struct browser *browser = data;
   1350 	if (browser->tofu_mode != TOFU_ASK) {
   1351 		return browser->tofu_mode;
   1352 	}
   1353 
   1354 	static char prompt[8192];
   1355 	switch (error) {
   1356 	case TOFU_VALID:
   1357 		assert(0); // Invariant
   1358 	case TOFU_INVALID_CERT:
   1359 		snprintf(prompt, sizeof(prompt),
   1360 			"The certificate offered by this server IS INVALID.\n"
   1361 			"/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"
   1362 			"If you choose to proceed, you should not disclose personal information or trust "
   1363 			"the contents of the page.\n"
   1364 			"[a]bort; trust [o]nce\n"
   1365 			"=> ");
   1366 		break;
   1367 	case TOFU_UNTRUSTED_CERT:;
   1368 		char *host;
   1369 		if (curl_url_get(browser->url, CURLUPART_HOST, &host, 0) != CURLUE_OK) {
   1370 			fprintf(stderr, "Error: invalid URL %s\n",
   1371 				browser->plain_url);
   1372 			return TOFU_FAIL;
   1373 		}
   1374 		snprintf(prompt, sizeof(prompt),
   1375 			"The certificate offered by %s is of unknown trust. "
   1376 			"Its fingerprint is: \n"
   1377 			"%s\n\n"
   1378 			"If you knew the fingerprint to expect in advance, verify that this matches.\n"
   1379 			"Otherwise, it should be safe to trust this certificate.\n\n"
   1380 			"[t]rust always; trust [o]nce; [a]bort\n"
   1381 			"=> ", host, fingerprint);
   1382 		free(host);
   1383 		break;
   1384 	case TOFU_FINGERPRINT_MISMATCH:
   1385 		snprintf(prompt, sizeof(prompt),
   1386 			"The certificate offered by this server DOES NOT MATCH the one we have on file.\n"
   1387 			"/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"
   1388 			"The unknown certificate's fingerprint is:\n"
   1389 			"%s\n\n"
   1390 			"The expected fingerprint is:\n"
   1391 			"%s\n\n"
   1392 			"If you choose to proceed, you should not disclose personal information or trust "
   1393 			"the contents of the page.\n"
   1394 			"[a]bort; trust [o]nce; [t]rust anyway\n"
   1395 			"=> ", fingerprint, khost->fingerprint);
   1396 		break;
   1397 	}
   1398 
   1399 	bool prompting = true;
   1400 	while (prompting) {
   1401 		fprintf(browser->tty, "%s", prompt);
   1402 
   1403 		size_t sz = 0;
   1404 		char *line = NULL;
   1405 		if (getline(&line, &sz, browser->tty) == -1) {
   1406 			free(line);
   1407 			return TOFU_FAIL;
   1408 		}
   1409 		if (line[1] != '\n') {
   1410 			free(line);
   1411 			continue;
   1412 		}
   1413 
   1414 		char c = line[0];
   1415 		free(line);
   1416 
   1417 		switch (c) {
   1418 		case 't':
   1419 			if (error == TOFU_INVALID_CERT) {
   1420 				break;
   1421 			}
   1422 			return TOFU_TRUST_ALWAYS;
   1423 		case 'o':
   1424 			return TOFU_TRUST_ONCE;
   1425 		case 'a':
   1426 			return TOFU_FAIL;
   1427 		}
   1428 	}
   1429 
   1430 	return TOFU_FAIL;
   1431 }
   1432 
   1433 int
   1434 main(int argc, char *argv[])
   1435 {
   1436 	struct browser browser = {
   1437 		.pagination = true,
   1438 		.alttext = false,
   1439 		.autoopen = false,
   1440 		.tofu_mode = TOFU_ASK,
   1441 		.unicode = true,
   1442 		.url = curl_url(),
   1443 		.tty = fopen("/dev/tty", "w+"),
   1444 		.meta = NULL,
   1445 		.max_redirs = REDIRS_ASK,
   1446 	};
   1447 
   1448 	int c;
   1449 	while ((c = getopt(argc, argv, "hj:PR:UW:TA")) != -1) {
   1450 		switch (c) {
   1451 		case 'h':
   1452 			usage(argv[0]);
   1453 			return 0;
   1454 		case 'j':
   1455 			if (strcmp(optarg, "fail") == 0) {
   1456 				browser.tofu_mode = TOFU_FAIL;
   1457 			} else if (strcmp(optarg, "once") == 0) {
   1458 				browser.tofu_mode = TOFU_TRUST_ONCE;
   1459 			} else if (strcmp(optarg, "always") == 0) {
   1460 				browser.tofu_mode = TOFU_TRUST_ALWAYS;
   1461 			} else {
   1462 				usage(argv[0]);
   1463 				return 1;
   1464 			}
   1465 			break;
   1466 		case 'T':
   1467 			browser.autoopen = true;
   1468 			break;
   1469 		case 'A':
   1470 			browser.alttext = true;
   1471 			break;
   1472 		case 'P':
   1473 			browser.pagination = false;
   1474 			break;
   1475 		case 'R':;
   1476 			int mr = strtol(optarg, NULL, 10);
   1477 			browser.max_redirs = mr < 0 ? REDIRS_UNLIMITED : mr;
   1478 			break;
   1479 		case 'U':
   1480 			browser.unicode = false;
   1481 			break;
   1482 		case 'W':
   1483 			browser.max_width = strtoul(optarg, NULL, 10);
   1484 			break;
   1485 		default:
   1486 			fprintf(stderr, "fatal: unknown flag %c\n", c);
   1487 			curl_url_cleanup(browser.url);
   1488 			return 1;
   1489 		}
   1490 	}
   1491 
   1492 	if (optind == argc - 1) {
   1493 		if (!set_url(&browser, argv[optind], &browser.history)) {
   1494 			return 1;
   1495 		}
   1496 	} else {
   1497 		open_bookmarks(&browser);
   1498 	}
   1499 
   1500 	gemini_tofu_init(&browser.tofu, &tofu_callback, &browser);
   1501 
   1502 	struct gemini_response resp;
   1503 	browser.running = true;
   1504 	while (browser.running) {
   1505 		static char prompt[4096];
   1506 		bool skip_prompt = do_requests(&browser, &resp) == GEMINI_OK
   1507 			&& display_response(&browser, &resp);
   1508 		if (browser.meta) {
   1509 			free(browser.meta);
   1510 		}
   1511 		browser.meta = resp.status == GEMINI_STATUS_SUCCESS
   1512 			? strdup(resp.meta) : NULL;
   1513 		gemini_response_finish(&resp);
   1514 		if (!skip_prompt) {
   1515 			char *end = NULL;
   1516 			if (browser.meta && (end = strchr(browser.meta, ';')) != NULL) {
   1517 				*end = 0;
   1518 			}
   1519 			snprintf(prompt, sizeof(prompt), "\n%s at %s\n"
   1520 				"[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n"
   1521 				"=> ", browser.meta ? browser.meta
   1522 				: "[request failed]", browser.plain_url,
   1523 				browser.history->prev ? "[b]ack; " : "",
   1524 				browser.history->next ? "[f]orward; " : "");
   1525 			if (end != NULL) {
   1526 				*end = ';';
   1527 			}
   1528 
   1529 			enum prompt_result result = PROMPT_AGAIN;
   1530 			while (result == PROMPT_AGAIN || result == PROMPT_MORE) {
   1531 				result = do_prompts(prompt, &browser);
   1532 			}
   1533 			switch (result) {
   1534 			case PROMPT_AGAIN:
   1535 			case PROMPT_MORE:
   1536 				assert(0);
   1537 			case PROMPT_QUIT:
   1538 				browser.running = false;
   1539 				break;
   1540 			case PROMPT_ANSWERED:
   1541 			case PROMPT_NEXT:
   1542 				break;
   1543 			}
   1544 		}
   1545 
   1546 		struct link *link = browser.links;
   1547 		while (link) {
   1548 			struct link *next = link->next;
   1549 			free(link->url);
   1550 			free(link);
   1551 			link = next;
   1552 		}
   1553 		browser.links = NULL;
   1554 	}
   1555 
   1556 	gemini_tofu_finish(&browser.tofu);
   1557 	struct history *hist = browser.history;
   1558 	while (hist && hist->prev) {
   1559 		hist = hist->prev;
   1560 	}
   1561 	history_free(hist);
   1562 	curl_url_cleanup(browser.url);
   1563 	free(browser.page_title);
   1564 	free(browser.plain_url);
   1565 	if (browser.meta != NULL) {
   1566 		free(browser.meta);
   1567 	}
   1568 	fclose(browser.tty);
   1569 	return 0;
   1570 }