cgmnlm

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

gmnlm.c (38016B)


      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 	struct winsize ws;
   1086 	ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);
   1087 	if (browser->max_width != 0 && ws.ws_col > browser->max_width) {
   1088 		ws.ws_col = browser->max_width;
   1089 	}
   1090 
   1091 	FILE *out = browser->tty;
   1092 	bool searching = browser->searching;
   1093 	if (searching) {
   1094 		out = fopen("/dev/null", "w+");
   1095 	}
   1096 
   1097 	fprintf(out, "\n");
   1098 	char *text = NULL;
   1099 	int row = 0, col = 0;
   1100 	bool alttext_printed = false;
   1101 	struct gemini_token tok;
   1102 	struct link **next = &browser->links;
   1103 	// When your screen is too narrow, more lines will be used for helptext and URL.
   1104 	// 87 is the maximum width of the prompt.
   1105 	int info_rows = (ws.ws_col >= 87) ? 4 : 6;
   1106 
   1107 	while (text != NULL || gemini_parser_next(&p, &tok) == 0) {
   1108 repeat:
   1109 		switch (tok.token) {
   1110 		case GEMINI_TEXT:
   1111 			col += fprintf(out, "     ");
   1112 			if (text == NULL) {
   1113 				text = tok.text;
   1114 			}
   1115 			break;
   1116 		case GEMINI_LINK:
   1117 			if (text == NULL) {
   1118 				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));
   1119 				text = trim_ws(tok.link.text ? tok.link.text : tok.link.url);
   1120 				*next = calloc(1, sizeof(struct link));
   1121 				(*next)->url = strdup(trim_ws(tok.link.url));
   1122 				next = &(*next)->next;
   1123 			} else {
   1124 				col += fprintf(out, "     ");
   1125 			}
   1126 			break;
   1127 		case GEMINI_PREFORMATTED_BEGIN:
   1128 			alttext_printed = false;
   1129 			if (text == NULL && browser->alttext && tok.preformatted != NULL) {
   1130 				fprintf(out, "   A %s", ANSI_COLOR_GRAY);
   1131 				text = trim_ws(tok.preformatted);
   1132 				alttext_printed = true;
   1133 			}
   1134 			break;
   1135 			/* fallthrough */
   1136 		case GEMINI_PREFORMATTED_END:
   1137 			continue; // Not used
   1138 		case GEMINI_PREFORMATTED_TEXT:
   1139 			if (alttext_printed) continue;
   1140 			if (text == NULL) {
   1141 				fprintf(out, "   P %s", ANSI_COLOR_GRAY);
   1142 				text = tok.preformatted;
   1143 			}
   1144 			break;
   1145 		case GEMINI_HEADING:
   1146 			if (!browser->page_title) {
   1147 				browser->page_title = strdup(tok.heading.title);
   1148 			}
   1149 			if (text == NULL) {
   1150 				switch (tok.heading.level) {
   1151 				case 1:
   1152 					col += fprintf(out, "   # %s", ANSI_COLOR_RED);
   1153 					break;
   1154 				case 2:
   1155 					col += fprintf(out, "  ## %s", ANSI_COLOR_YELLOW);
   1156 					break;
   1157 				case 3:
   1158 					col += fprintf(out, " ### %s", ANSI_COLOR_GREEN);
   1159 					break;
   1160 				}
   1161 				text = trim_ws(tok.heading.title);
   1162 			} else {
   1163 				col += fprintf(out, "     ");
   1164 			}
   1165 			break;
   1166 		case GEMINI_LIST_ITEM:
   1167 			if (text == NULL) {
   1168 				col += fprintf(out, "   %s ",
   1169 					browser->unicode ? "•" : "*");
   1170 				text = trim_ws(tok.list_item);
   1171 			} else {
   1172 				col += fprintf(out, "     ");
   1173 			}
   1174 			break;
   1175 		case GEMINI_QUOTE:
   1176 			col += fprintf(out, "   %s ", browser->unicode ? "┃" : ">");
   1177 			if (text == NULL) {
   1178 				text = trim_ws(tok.quote_text);
   1179 			}
   1180 			break;
   1181 		}
   1182 
   1183 		if (text && searching) {
   1184 			int r = regexec(&browser->regex, text, 0, NULL, 0);
   1185 			if (r != 0) {
   1186 				text = NULL;
   1187 				continue;
   1188 			} else {
   1189 				fclose(out);
   1190 				row = col = 0;
   1191 				out = browser->tty;
   1192 				text = NULL;
   1193 				searching = false;
   1194 				goto repeat;
   1195 			}
   1196 		}
   1197 
   1198 		if (text) {
   1199 			int w = wrap(out, text, &ws, &row, &col);
   1200 			text += w;
   1201 			if (text[0] && row < ws.ws_row - info_rows) {
   1202 				continue;
   1203 			}
   1204 
   1205 			if (!text[0]) {
   1206 				text = NULL;
   1207 			}
   1208 		}
   1209 		if (text == NULL) {
   1210 			gemini_token_finish(&tok);
   1211 		}
   1212 
   1213 		while (col >= ws.ws_col) {
   1214 			col -= ws.ws_col;
   1215 			++row;
   1216 		}
   1217 		++row; col = 0;
   1218 
   1219 		fprintf(out, ANSI_COLOR_RESET);
   1220 		if (browser->pagination && row >= ws.ws_row - info_rows) {
   1221 			char prompt[4096];
   1222 			char *end = NULL;
   1223 			if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) {
   1224 				*end = 0;
   1225 			}
   1226 			snprintf(prompt, sizeof(prompt), "%s at %s\n"
   1227 				"[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n"
   1228 				"(more) => ", resp->meta, browser->plain_url,
   1229 				browser->searching ? "[n]ext result; " : "",
   1230 				browser->history->prev ? "[b]ack; " : "",
   1231 				browser->history->next ? "[f]orward; " : "");
   1232 			if (end != NULL) {
   1233 				*end = ';';
   1234 			}
   1235 			enum prompt_result result = PROMPT_AGAIN;
   1236 			while (result == PROMPT_AGAIN) {
   1237 				result = do_prompts(prompt, browser);
   1238 			}
   1239 
   1240 			switch (result) {
   1241 			case PROMPT_AGAIN:
   1242 			case PROMPT_MORE:
   1243 				printf("\n");
   1244 				break;
   1245 			case PROMPT_QUIT:
   1246 				browser->running = false;
   1247 				if (text != NULL) {
   1248 					gemini_token_finish(&tok);
   1249 				}
   1250 				gemini_parser_finish(&p);
   1251 				return true;
   1252 			case PROMPT_ANSWERED:
   1253 				if (text != NULL) {
   1254 					gemini_token_finish(&tok);
   1255 				}
   1256 				gemini_parser_finish(&p);
   1257 				return true;
   1258 			case PROMPT_NEXT:
   1259 				searching = true;
   1260 				out = fopen("/dev/null", "w");
   1261 				break;
   1262 			}
   1263 
   1264 			row = col = 0;
   1265 		}
   1266 	}
   1267 
   1268 	gemini_token_finish(&tok);
   1269 	gemini_parser_finish(&p);
   1270 	return false;
   1271 }
   1272 
   1273 static bool
   1274 display_plaintext(struct browser *browser, struct gemini_response *resp)
   1275 {
   1276 	struct winsize ws;
   1277 	int row = 0, col = 0;
   1278 	ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);
   1279 
   1280 	char buf[BUFSIZ];
   1281 	for (int n = 1; n > 0;) {
   1282 		if (resp->sc) {
   1283 			n = br_sslio_read(&resp->body, buf, BUFSIZ);
   1284 		} else {
   1285 			n = read(resp->fd, buf, BUFSIZ);
   1286 		}
   1287 		if (n < 0) {
   1288 			n = 0;
   1289 		}
   1290 		for (int i = 0; i < n; i++) {
   1291 			if (iscntrl(buf[i]) && (buf[i] < '\t' || buf[i] > '\v')) {
   1292 				buf[i] = '.';
   1293 			}
   1294 		}
   1295 		ssize_t w = 0;
   1296 		while (w < (ssize_t)n) {
   1297 			ssize_t x = fwrite(&buf[w], 1, n - w, browser->tty);
   1298 			if (ferror(browser->tty)) {
   1299 				fprintf(stderr, "Error: write: %s\n",
   1300 					strerror(errno));
   1301 				return 1;
   1302 			}
   1303 			w += x;
   1304 		}
   1305 	}
   1306 
   1307 	(void)row; (void)col; // TODO: generalize pagination
   1308 	return false;
   1309 }
   1310 
   1311 static bool
   1312 display_response(struct browser *browser, struct gemini_response *resp)
   1313 {
   1314 	if (gemini_response_class(resp->status) != GEMINI_STATUS_CLASS_SUCCESS) {
   1315 		return false;
   1316 	}
   1317 	printf("%c]0;%s%s%c", '\033', browser->plain_url, " - cgmnlm", '\007');
   1318 	if (strcmp(resp->meta, "text/gemini") == 0
   1319 			|| strncmp(resp->meta, "text/gemini;", 12) == 0) {
   1320 		return display_gemini(browser, resp);
   1321 	}
   1322 	if (strncmp(resp->meta, "text/", 5) == 0) {
   1323 		return display_plaintext(browser, resp);
   1324 	}
   1325 	fprintf(stderr, "Media type %s is unsupported, use \"d [path]\" to download this page\n",
   1326 		resp->meta);
   1327 	return false;
   1328 }
   1329 
   1330 static enum tofu_action
   1331 tofu_callback(enum tofu_error error, const char *fingerprint,
   1332 	struct known_host *khost, void *data)
   1333 {
   1334 	struct browser *browser = data;
   1335 	if (browser->tofu_mode != TOFU_ASK) {
   1336 		return browser->tofu_mode;
   1337 	}
   1338 
   1339 	static char prompt[8192];
   1340 	switch (error) {
   1341 	case TOFU_VALID:
   1342 		assert(0); // Invariant
   1343 	case TOFU_INVALID_CERT:
   1344 		snprintf(prompt, sizeof(prompt),
   1345 			"The certificate offered by this server IS INVALID.\n"
   1346 			"/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"
   1347 			"If you choose to proceed, you should not disclose personal information or trust "
   1348 			"the contents of the page.\n"
   1349 			"[a]bort; trust [o]nce\n"
   1350 			"=> ");
   1351 		break;
   1352 	case TOFU_UNTRUSTED_CERT:;
   1353 		char *host;
   1354 		if (curl_url_get(browser->url, CURLUPART_HOST, &host, 0) != CURLUE_OK) {
   1355 			fprintf(stderr, "Error: invalid URL %s\n",
   1356 				browser->plain_url);
   1357 			return TOFU_FAIL;
   1358 		}
   1359 		snprintf(prompt, sizeof(prompt),
   1360 			"The certificate offered by %s is of unknown trust. "
   1361 			"Its fingerprint is: \n"
   1362 			"%s\n\n"
   1363 			"If you knew the fingerprint to expect in advance, verify that this matches.\n"
   1364 			"Otherwise, it should be safe to trust this certificate.\n\n"
   1365 			"[t]rust always; trust [o]nce; [a]bort\n"
   1366 			"=> ", host, fingerprint);
   1367 		free(host);
   1368 		break;
   1369 	case TOFU_FINGERPRINT_MISMATCH:
   1370 		snprintf(prompt, sizeof(prompt),
   1371 			"The certificate offered by this server DOES NOT MATCH the one we have on file.\n"
   1372 			"/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"
   1373 			"The unknown certificate's fingerprint is:\n"
   1374 			"%s\n\n"
   1375 			"The expected fingerprint is:\n"
   1376 			"%s\n\n"
   1377 			"If you choose to proceed, you should not disclose personal information or trust "
   1378 			"the contents of the page.\n"
   1379 			"[a]bort; trust [o]nce; [t]rust anyway\n"
   1380 			"=> ", fingerprint, khost->fingerprint);
   1381 		break;
   1382 	}
   1383 
   1384 	bool prompting = true;
   1385 	while (prompting) {
   1386 		fprintf(browser->tty, "%s", prompt);
   1387 
   1388 		size_t sz = 0;
   1389 		char *line = NULL;
   1390 		if (getline(&line, &sz, browser->tty) == -1) {
   1391 			free(line);
   1392 			return TOFU_FAIL;
   1393 		}
   1394 		if (line[1] != '\n') {
   1395 			free(line);
   1396 			continue;
   1397 		}
   1398 
   1399 		char c = line[0];
   1400 		free(line);
   1401 
   1402 		switch (c) {
   1403 		case 't':
   1404 			if (error == TOFU_INVALID_CERT) {
   1405 				break;
   1406 			}
   1407 			return TOFU_TRUST_ALWAYS;
   1408 		case 'o':
   1409 			return TOFU_TRUST_ONCE;
   1410 		case 'a':
   1411 			return TOFU_FAIL;
   1412 		}
   1413 	}
   1414 
   1415 	return TOFU_FAIL;
   1416 }
   1417 
   1418 int
   1419 main(int argc, char *argv[])
   1420 {
   1421 	struct browser browser = {
   1422 		.pagination = true,
   1423 		.alttext = false,
   1424 		.autoopen = false,
   1425 		.tofu_mode = TOFU_ASK,
   1426 		.unicode = true,
   1427 		.url = curl_url(),
   1428 		.tty = fopen("/dev/tty", "w+"),
   1429 		.meta = NULL,
   1430 		.max_redirs = REDIRS_ASK,
   1431 	};
   1432 
   1433 	int c;
   1434 	while ((c = getopt(argc, argv, "hj:PR:UW:TA")) != -1) {
   1435 		switch (c) {
   1436 		case 'h':
   1437 			usage(argv[0]);
   1438 			return 0;
   1439 		case 'j':
   1440 			if (strcmp(optarg, "fail") == 0) {
   1441 				browser.tofu_mode = TOFU_FAIL;
   1442 			} else if (strcmp(optarg, "once") == 0) {
   1443 				browser.tofu_mode = TOFU_TRUST_ONCE;
   1444 			} else if (strcmp(optarg, "always") == 0) {
   1445 				browser.tofu_mode = TOFU_TRUST_ALWAYS;
   1446 			} else {
   1447 				usage(argv[0]);
   1448 				return 1;
   1449 			}
   1450 			break;
   1451 		case 'T':
   1452 			browser.autoopen = true;
   1453 			break;
   1454 		case 'A':
   1455 			browser.alttext = true;
   1456 			break;
   1457 		case 'P':
   1458 			browser.pagination = false;
   1459 			break;
   1460 		case 'R':;
   1461 			int mr = strtol(optarg, NULL, 10);
   1462 			browser.max_redirs = mr < 0 ? REDIRS_UNLIMITED : mr;
   1463 			break;
   1464 		case 'U':
   1465 			browser.unicode = false;
   1466 			break;
   1467 		case 'W':
   1468 			browser.max_width = strtoul(optarg, NULL, 10);
   1469 			break;
   1470 		default:
   1471 			fprintf(stderr, "fatal: unknown flag %c\n", c);
   1472 			curl_url_cleanup(browser.url);
   1473 			return 1;
   1474 		}
   1475 	}
   1476 
   1477 	if (optind == argc - 1) {
   1478 		if (!set_url(&browser, argv[optind], &browser.history)) {
   1479 			return 1;
   1480 		}
   1481 	} else {
   1482 		open_bookmarks(&browser);
   1483 	}
   1484 
   1485 	gemini_tofu_init(&browser.tofu, &tofu_callback, &browser);
   1486 
   1487 	struct gemini_response resp;
   1488 	browser.running = true;
   1489 	while (browser.running) {
   1490 		static char prompt[4096];
   1491 		bool skip_prompt = do_requests(&browser, &resp) == GEMINI_OK
   1492 			&& display_response(&browser, &resp);
   1493 		if (browser.meta) {
   1494 			free(browser.meta);
   1495 		}
   1496 		browser.meta = resp.status == GEMINI_STATUS_SUCCESS
   1497 			? strdup(resp.meta) : NULL;
   1498 		gemini_response_finish(&resp);
   1499 		if (!skip_prompt) {
   1500 			char *end = NULL;
   1501 			if (browser.meta && (end = strchr(browser.meta, ';')) != NULL) {
   1502 				*end = 0;
   1503 			}
   1504 			snprintf(prompt, sizeof(prompt), "\n%s at %s\n"
   1505 				"[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n"
   1506 				"=> ", browser.meta ? browser.meta
   1507 				: "[request failed]", browser.plain_url,
   1508 				browser.history->prev ? "[b]ack; " : "",
   1509 				browser.history->next ? "[f]orward; " : "");
   1510 			if (end != NULL) {
   1511 				*end = ';';
   1512 			}
   1513 
   1514 			enum prompt_result result = PROMPT_AGAIN;
   1515 			while (result == PROMPT_AGAIN || result == PROMPT_MORE) {
   1516 				result = do_prompts(prompt, &browser);
   1517 			}
   1518 			switch (result) {
   1519 			case PROMPT_AGAIN:
   1520 			case PROMPT_MORE:
   1521 				assert(0);
   1522 			case PROMPT_QUIT:
   1523 				browser.running = false;
   1524 				break;
   1525 			case PROMPT_ANSWERED:
   1526 			case PROMPT_NEXT:
   1527 				break;
   1528 			}
   1529 		}
   1530 
   1531 		struct link *link = browser.links;
   1532 		while (link) {
   1533 			struct link *next = link->next;
   1534 			free(link->url);
   1535 			free(link);
   1536 			link = next;
   1537 		}
   1538 		browser.links = NULL;
   1539 	}
   1540 
   1541 	gemini_tofu_finish(&browser.tofu);
   1542 	struct history *hist = browser.history;
   1543 	while (hist && hist->prev) {
   1544 		hist = hist->prev;
   1545 	}
   1546 	history_free(hist);
   1547 	curl_url_cleanup(browser.url);
   1548 	free(browser.page_title);
   1549 	free(browser.plain_url);
   1550 	if (browser.meta != NULL) {
   1551 		free(browser.meta);
   1552 	}
   1553 	fclose(browser.tty);
   1554 	return 0;
   1555 }