gmni

a gemini line mode client
git clone https://git.clttr.info/gmni.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

gmnlm.c (33316B)


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