// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 1997 Michael R. Elkins <me@cs.hmc.edu>
// Created:       Thu Dec  4 02:13:11 PST 1997
// Last Modified: Tue Jul  4 11:23:49 CEST 2000


#include <assert.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <locale.h>
#include <ncurses.h>
#include <regex.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ttydefaults.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
#if __has_include(<libintl.h>)
#include <libintl.h>
#else
#define gettext(s) (s)
#define ngettext(s, p, n) (n == 1 ? s : p)
#endif

#include "enter.h"
#include "quote.h"
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define auto __auto_type
#ifndef REG_STARTEND
#define REG_STARTEND_OPT 0
#else
#define REG_STARTEND_OPT REG_STARTEND
#endif


enum redraw { FULL, INDEX, MOTION, CURSORPOS, NO };
struct url {
	char * url;
	size_t urllen;
	int cursor_y, cursor_x;
	int last_y;
};

static char * regex_error(const regex_t * rx, int err) {
	size_t es = regerror(err, rx, NULL, 0);
	char * eb = malloc(es);
	if(eb)
		regerror(err, rx, eb, es);
	return eb;
}

static void search_common(const char * search, regex_t * rx, size_t urlcount, struct url * url, enum redraw * redraw, size_t * current,
                          bool (*matcher)(const regex_t * rx, size_t urlcount, struct url * url, size_t * current)) {
	if(search) {
		regex_t newrx;
		int regerr = regcomp(&newrx, search, REG_EXTENDED | REG_ICASE | REG_NOSUB);
		if(regerr) {
			char * err = regex_error(&newrx, regerr);
			mvaddstr(LINES - 1, 0, search);
			addstr(": ");
			addstr(err ?: "?");
			clrtoeol();
			free(err);
			*redraw = CURSORPOS;
			return;
		}

		if(rx->re_nsub != (size_t)-1)
			regfree(rx);
		*rx = newrx;
	}

	if(matcher(rx, urlcount, url, current)) {
		move(LINES - 1, 0);
		*redraw = MOTION;
	} else {
		// as in "no search results", p.a. to "No such URL!"; potentially displayed after "Search [forwards/backwards] for:"
		mvaddstr(LINES - 1, 0, gettext("No matches!"));
		*redraw = CURSORPOS;
	}
	clrtoeol();
}

static bool search_impl(const regex_t * rx, struct url * url) {
	return !regexec(rx, url->url, 0, &(regmatch_t){.rm_so = 0, .rm_eo = url->urllen}, REG_STARTEND_OPT);
}

static bool search_forward(const regex_t * rx, size_t urlcount, struct url * url, size_t * current) {
	for(size_t i = *current + 1; i < urlcount; ++i)
		if(search_impl(rx, &url[i])) {
			*current = i;
			return true;
		}
	return false;
}

static bool search_backward(const regex_t * rx, size_t urlcount, struct url * url, size_t * current) {
	(void)urlcount;

	for(size_t i = *current - 1; i != (size_t)-1; --i)
		if(search_impl(rx, &url[i])) {
			*current = i;
			return true;
		}
	return false;
}

enum via { ARGUMENT, ENVIRONMENT, PIPE };
// if ARGUMENT, program must have already been pre-processed
static int system_via(const char * program, const char * url, enum via via) {
	struct sigaction intr, quit;
	sigaction(SIGINT, &(struct sigaction){.sa_handler = SIG_IGN}, &intr);
	sigaction(SIGQUIT, &(struct sigaction){.sa_handler = SIG_IGN}, &quit);

	int pfd[2];
	if(via == PIPE)
		if(pipe2(pfd, O_CLOEXEC) == -1)
			return -1;

	pid_t pid = fork();
	switch(pid) {
		case -1:
			return -1;
		case 0:  // child
			sigaction(SIGINT, &intr, NULL);
			sigaction(SIGQUIT, &quit, NULL);
			switch(via) {
				case ARGUMENT:
					break;
				case ENVIRONMENT:
					setenv("URL", url, true);
					break;
				case PIPE:
					dup2(pfd[0], 0);
					break;
			}
			execl("/bin/sh", "sh", "-c", "--", program, (char *)NULL);
			_exit(errno == ENOENT ? 127 : 126);
	}

	if(via == PIPE) {
		close(pfd[0]);
		struct iovec iovs[2] = {{.iov_base = (void *)url, .iov_len = strlen(url)}, {.iov_base = "\n", .iov_len = 1}};
		for(off_t wr;;) {
			while((wr = writev(pfd[1], iovs, 2)) == -1 && errno == EINTR)
				;
			if(wr == -1)
				break;

			if((size_t)wr > iovs[0].iov_len)
				break;
			iovs[0].iov_base += wr;
			iovs[0].iov_len -= wr;
		}
		close(pfd[1]);
	}

	int status;
	while(waitpid(pid, &status, 0) == -1)  // EINTR
		;
	sigaction(SIGINT, &intr, NULL);
	sigaction(SIGINT, &quit, NULL);
	return status;
}

int main(int argc, char ** argv) {
	setlocale(LC_ALL, "");
#if __has_include(<libintl.h>)
	bindtextdomain("urlview-ng", TEXTDOMAIN_DIRNAME);
	textdomain("urlview-ng");
#endif

	/*** read the initialization file ***/
	char * command     = NULL;
	char * regexp      = NULL;
	bool expert        = false;
	bool skip_browser  = false;
	bool menu_wrapping = false;
	bool edit_url      = false;
	bool mouse         = true;
	enum via via       = ARGUMENT;

	FILE * fp   = NULL;
	char * home = getenv("HOME");
	if(home) {
		char * fpp;
		if(asprintf(&fpp, "%s/.urlview", home) != -1) {
			fp = fopen(fpp, "re");
			free(fpp);
		}
	}
	if(!fp)
		fp = fopen(SYSTEM_INITFILE, "re");

	char * line    = NULL;
	size_t linecap = 0;
	if(fp) {
		for(ssize_t len; (len = getline(&line, &linecap, fp)) != -1;) {
			if(line[0] == '#' || line[0] == '\n')
				continue;
			if(line[len - 1] == '\n')
				line[--len] = '\0';

			size_t keylen   = strcspn(line, " \f\n\r\t\v");
			size_t seplen   = strspn(line + keylen, " \f\n\r\t\v");
			char * key      = line;
			char * value    = line + keylen + seplen;
			size_t valuelen = len - (keylen + seplen);

#define BOOLKEY(name, var)                                                                                                                                  \
	if(KEYMATCH(name)) {                                                                                                                                      \
		if(!strcasecmp("YES", value))                                                                                                                           \
			var = true;                                                                                                                                           \
		else if(!strcasecmp("NO", value))                                                                                                                       \
			var = false;                                                                                                                                          \
		else /* "BROWSER", user's input, "yes, no" */                                                                                                           \
			errx(1, gettext("Unknown value for %s: %s. Valid values are: %s."), name, value, /* leave yes/no intact, but add explanations */ gettext("yes, no")); \
	}

#define KEYMATCH(k) keylen == sizeof(k) - 1 && !memcmp(key, k, sizeof(k) - 1)
			if(KEYMATCH("REGEXP") && valuelen) {
				if(!(regexp = realloc(regexp, valuelen)))
					err(1, NULL);
				char * wc = regexp;
				while(*value) {
					if(*value == '\\') {
						value++;
						switch(*value) {
							case 'n':
								*wc++ = '\n';
								break;
							case 'r':
								*wc++ = '\r';
								break;
							case 't':
								*wc++ = '\t';
								break;
							case 'f':
								*wc++ = '\f';
								break;
							default:
								*wc++ = '\\';
								*wc++ = *value;
								break;
						}
					} else
						*wc++ = *value;
					++value;
				}
				*wc = '\0';
			} else if(KEYMATCH("COMMAND") && valuelen) {
				free(command);
				if(!(command = strdup(value)))
					err(1, NULL);
				skip_browser = true;
			} else
				BOOLKEY("WRAP", menu_wrapping)
			else                           //
			    BOOLKEY("EDIT", edit_url)  //
			    else                       //
			    BOOLKEY("MOUSE", mouse)    //
			    else if(KEYMATCH("VIA")) {
				if(!strcasecmp("ARGUMENT", value))
					via = ARGUMENT;
				else if(!strcasecmp("ENVIRONMENT", value))
					via = ENVIRONMENT;
				else if(!strcasecmp("PIPE", value))
					via = PIPE;
				else /* "VIA", user's input, "argument, environment, pipe" */
					errx(1, gettext("Unknown value for %s: %s. Valid values are: %s."), "VIA", value,
					     /*leave argument/environment/pipe intact, but add explanations*/ gettext("argument, environment, pipe"));
			}
			else if(KEYMATCH("BROWSER"))  //
			    skip_browser = false;
			else if(KEYMATCH("EXPERT"))  //
			    expert = true;
			else  //
			    errx(1, gettext("Unknown setting: %s"), line);
		}
		fclose(fp);
	}

	/* Only use the $BROWSER environment variable if
	 * (a) no COMMAND in rc file or
	 * (b) BROWSER in rc file.
	 * If both COMMAND and BROWSER are in the rc file, then the option used
	 * last counts.
	 */
	if(!skip_browser) {
		char * browser = getenv("BROWSER");
		if(browser) {
			if(*browser)
				command = browser;
			else
				warnx(gettext("Empty $BROWSER, falling back to COMMAND (%s)."), command ?: DEFAULT_COMMAND);
		}
	}
	if(!command)
		command = (char *)DEFAULT_COMMAND;

	if(via == ARGUMENT && !expert && strchr(command, '\''))
		errx(1, gettext("\n"
		                "Your COMMAND/$BROWSER contains a single quote (') character.\n"
		                "This is most likely in error; please read the manual page for details.\n"
		                "If you really want to use this command, please put the word EXPERT\n"
		                "into a line of its own in your ~/.urlview file."));

	/*** compile the regexp ***/
	regex_t rx;
	int regerr = regcomp(&rx, regexp ?: DEFAULT_REGEXP, REG_EXTENDED | REG_ICASE | REG_NEWLINE);
	if(regerr)
		errx(1, "%s: %s", regexp ?: DEFAULT_REGEXP, regex_error(&rx, regerr));
	free(regexp);

	/*** prepare arguments ***/
	off_t startline = 0;
	bool reopen_tty = false;
	size_t current = 0, oldcurrent = 0;

	opterr = false;
	for(int opt; (opt = getopt(argc, argv, "+0::1::2::3::4::5::6::7::8::9::")) != -1;) {
		if(opt == '?') {
			--optind;
			break;
		}


		if(optarg) {
			char * end;
			errno     = 0;
			startline = strtoll(optarg - 1, &end, 10);
			if(errno || *end) {
				errno = errno ? errno : EINVAL;
				err(1, "-%s", optarg - 1);
			}
		} else
			startline = opt - '0';
		current = (size_t)-1;
	}
	if(optind == argc)
		argv[--optind] = "-";

	/*** find matching patterns ***/
	struct url * url = NULL;
	size_t urlsize   = 0;
	size_t urlcount  = 0;
	bool error       = false;
	for(int i = optind; i < argc; ++i) {
		if(!strcmp("-", argv[i])) {
			fp         = stdin;
			reopen_tty = true;
		} else if(!(fp = fopen(argv[i], "re"))) {
			warn("%s", argv[i]);
			error = true;
			continue;
		}

		for(ssize_t linelen; (linelen = getline(&line, &linecap, fp)) != -1;) {
			--startline;
			size_t offset = 0;

			for(regmatch_t match; (match = (regmatch_t){.rm_so = 0, .rm_eo = linelen - offset}),
			                      !regexec(&rx, line + offset, 1, &match, (offset ? REG_NOTBOL : 0) | REG_STARTEND_OPT);) {
				size_t len = match.rm_eo - match.rm_so;
				if(urlcount >= urlsize) {
					size_t urlsizetmp = urlsize ? urlsize * 2 : 16;
					void * urltmp     = reallocarray(url, urlsizetmp, sizeof(*url));
					if(urltmp == NULL) {
					cantalloc:
						warn(gettext("couldn't allocate memory for additional URLs, only first %zu displayed"), urlsize);
						goto got_urls;
					} else {
						urlsize = urlsizetmp;
						url     = urltmp;
					}
				}
				if(!(url[urlcount].url = malloc(len + 1)))
					goto cantalloc;
				memcpy(url[urlcount].url, line + match.rm_so + offset, len);
				url[urlcount].url[len] = 0;
				url[urlcount].urllen   = len;

				for(size_t i = 0; i < urlcount; ++i)
					if(url[i].urllen == len && !strncasecmp(url[i].url, url[urlcount].url, len)) {
						--urlcount;
						break;
					}

				if(current == (size_t)-1 && startline <= 0)
					current = urlcount;
				++urlcount;
				offset += match.rm_eo;
			}
		}
	got_urls:
		if(fp != stdin)
			fclose(fp);
	}
	regfree(&rx);
	free(line);

	if(getenv("URLVIEW_DEBUG") || !isatty(1)) {
		for(size_t i = 0; i < urlcount; ++i)
			fwrite(url[i].url, 1, url[i].urllen, stdout), putchar('\n');
		return 0;
	}
	if(!urlcount) {
		puts(gettext("No URLs found."));
		return 1;
	}

	if(current == (size_t)-1)
		current = urlcount - 1;

	if(reopen_tty)
		if(!freopen("/dev/tty", "r", stdin))
			err(1, "/dev/tty");
	initscr();

	cbreak();
	noecho();
	curs_set(1);
	keypad(stdscr, TRUE);
	if(mouse) {
// OpenBSD shim: it ships with NCURSES_MOUSE_VERSION being 1, which excludes BUTTON[45]_PRESSED: https://builds.sr.ht/~nabijaczleweli/job/1064539
#ifndef BUTTON5_PRESSED
#define BUTTON5_PRESSED 0
#endif
#ifndef BUTTON4_PRESSED
#define BUTTON4_PRESSED 0
#endif
		mousemask(BUTTON1_CLICKED | BUTTON5_PRESSED | BUTTON4_PRESSED, NULL);
		mouseinterval(0);
	}

//                                      title line
#define urlswin_logical_height (LINES - 1 - 1)
	//                                        status line
	WINDOW * urlswin = newpad(LINES, COLS);
	if(!urlswin)
		return endwin(), 1;

	enum redraw redraw         = FULL;
	struct enter_string search = {};
	regex_t search_rx          = {.re_nsub = (size_t)-1};
	int page_of_current        = 0;
	size_t first_on_page       = 0;
	int fudge                  = 0;
	for(bool done = false; !done;) {
		switch(redraw) {
			case FULL: {
				wresize(urlswin, LINES, COLS);

				/* "N URLs" is on top of "Press q to quit" is on top of the branding
				 * "N URLs" is left margin, "Press q to quit" is right margin, branding goes to center of remaining
				 * This unfortunately means we have to draw "N URLs" and "Press q to quit" first to a scratch window to establish the width,
				 * (urlswin is good since it's cleared below anyway; this approach is recommended by Dickey: https://stackoverflow.com/a/70590135/2851815),
				 * then draw them in the right order to stdscr.
				 */
				clear();
				standout();

				mvwprintw(urlswin, 0, 0, ngettext("%zu URL", "%zu URLs", urlcount), urlcount);
				int urlswidth = getcurx(urlswin);

				cc_t intr = CINTR;
				struct termios termios;
				if(!tcgetattr(0, &termios))
					intr = termios.c_cc[VINTR];
				char intr_s[4] = {};  // M-^?
				if(intr != _POSIX_VDISABLE) {
					char * intr_cur = intr_s;
					if(intr & 0x80)
						*intr_cur++ = 'M', *intr_cur++ = '-';
					intr &= 0x7F;
					if(isprint(intr))
						*intr_cur++ = intr;
					else if(intr == 0x7F)
						*intr_cur++ = '^', *intr_cur++ = '?';
					else
						*intr_cur++ = '^', *intr_cur++ = intr + '@';
					mvwprintw(urlswin, 0, 0, gettext("Press q or %.*s to quit!"), (int)sizeof(intr_s), intr_s);
				} else
					mvwaddstr(urlswin, 0, 0, gettext("Press q to quit!"));

#define BRAND "urlview-ng " VERSION ""
				mvwaddstr(stdscr, 0, urlswidth + (COLS - getcurx(urlswin) - urlswidth) / 2 - (sizeof(BRAND) - 1) / 2, BRAND);
				if(intr != _POSIX_VDISABLE)
					mvwprintw(stdscr, 0, COLS - getcurx(urlswin), gettext("Press q or %.*s to quit!"), (int)sizeof(intr_s), intr_s);
				else
					mvwaddstr(stdscr, 0, COLS - getcurx(urlswin), gettext("Press q to quit!"));

				mvwprintw(stdscr, 0, 0, ngettext("%zu URL", "%zu URLs", urlcount), urlcount);

				standend();
				wnoutrefresh(stdscr);
			}
				__attribute__((fallthrough));
			case INDEX:
				wclear(urlswin);
				wmove(urlswin, 0, 0);
				for(size_t i = 0; i < urlcount; ++i) {
#define EXPAND()                                  \
	{                                               \
		int sizey, sizex;                             \
		getmaxyx(urlswin, sizey, sizex);              \
		if(wresize(urlswin, sizey * 2, sizex) == ERR) \
			break;                                      \
	}
#define EXPANDONSCROLL(...)         \
	if(__VA_ARGS__ != OK) {           \
		wmove(urlswin, starty, startx); \
		EXPAND();                       \
		goto again;                     \
	}
					int starty, startx;
					getyx(urlswin, starty, startx);

				again:
					EXPANDONSCROLL(waddstr(urlswin, "   "));
					getyx(urlswin, url[i].cursor_y, url[i].cursor_x);
					--url[i].cursor_x;
					EXPANDONSCROLL(wprintw(urlswin, "%4zu", i + 1));
					EXPANDONSCROLL(waddch(urlswin, ' '));
					EXPANDONSCROLL(waddnstr(urlswin, url[i].url, url[i].urllen));
					url[i].last_y = getcury(urlswin);

					if(i != urlcount - 1)
						if(waddch(urlswin, '\n') != OK) {
							EXPAND();
							waddch(urlswin, '\n');
						}
				}
				__attribute__((fallthrough));
			case MOTION:
				mvwaddstr(urlswin, url[oldcurrent].cursor_y, url[oldcurrent].cursor_x - 2, "  ");
				wstandout(urlswin);
				mvwaddstr(urlswin, url[current].cursor_y, url[current].cursor_x - 2, "->");
				wstandend(urlswin);

				page_of_current = url[current].cursor_y / urlswin_logical_height;
				first_on_page   = 0;
				while(url[first_on_page].cursor_y / urlswin_logical_height != page_of_current)
					++first_on_page;
				fudge = 0;  // try to get the last-URL-on-the-page to be included when it's selected
				if(url[current].last_y - url[first_on_page].cursor_y > urlswin_logical_height)
					// unclear why +1 needed here; this means we overscroll (sometimes)
					// compare the first screen on a rows 54; cols 172; teletype of
					//   ./urlview text.uv <(for i in A B C D E F H I J K L M; do tr Q $i < text.uv; done)
					// and
					//   ./urlview text.uv <(for i in A B C D E F H I J K L M; do tr Q $i < text.uv; done | fold -w 300)
					// the last link on the first screen appear to have a completely identical last_y, but the former is taller by a line?
					fudge = (url[current].last_y - url[first_on_page].cursor_y) - urlswin_logical_height + 1;
				pnoutrefresh(urlswin, /**/ url[first_on_page].cursor_y + fudge, 0, /**/ 1 /*title line*/, 0, urlswin_logical_height, COLS);
				doupdate();

				oldcurrent = current;
				__attribute__((fallthrough));
			case CURSORPOS:
				// same projection as pnoutrefresh() above
				move(url[current].cursor_y - (url[first_on_page].cursor_y + fudge) + 1 /*title line*/, url[current].cursor_x - 0 + 0);
				__attribute__((fallthrough));
			case NO:
				break;
		}
		redraw = NO;

		int c = getch();
	rekey:
		switch(c) {
			case KEY_RESIZE:
				redraw = FULL;
				break;
			case KEY_MOUSE: {
				MEVENT ev;
				getmouse(&ev);

				if(ev.bstate & (BUTTON1_CLICKED | BUTTON1_DOUBLE_CLICKED | BUTTON1_TRIPLE_CLICKED)) {
					wmouse_trafo(urlswin, &ev.y, &ev.x, false);
					ev.y += url[first_on_page].cursor_y + fudge;

					for(size_t hit = first_on_page; hit != urlcount; ++hit)
						if(ev.y >= url[hit].cursor_y && ev.y <= url[hit].last_y) {
							current = hit;
							redraw  = MOTION;

							ungetch('\n');
							break;
						}
				} else if(ev.bstate & BUTTON5_PRESSED) {
					c = KEY_DOWN;
					goto rekey;
				} else if(ev.bstate & BUTTON4_PRESSED) {
					c = KEY_UP;
					goto rekey;
				}
			} break;
			case 'q':
			case 'x':
			case 'h':
				done = true;
				break;
			case KEY_DOWN:
			case 'j':
				if(current < urlcount - 1)
					++current;
				else {
					if(menu_wrapping)
						current = 0;
				}
				redraw = MOTION;
				break;
			case KEY_UP:
			case 'k':
				if(current)
					--current;
				else {
					if(menu_wrapping)
						current = urlcount - 1;
				}
				redraw = MOTION;
				break;
			case KEY_HOME:
			case '=':
				current = 0;
				redraw  = MOTION;
				break;
			case KEY_END:
			case '*':
			case 'G':
				current = urlcount - 1;
				redraw  = MOTION;
				break;
			case KEY_NPAGE:
			case '\006': {
				size_t first_on_next_page = first_on_page;
				while(first_on_next_page != urlcount - 1 && url[first_on_next_page].cursor_y / urlswin_logical_height != page_of_current + 1)
					++first_on_next_page;
				current = first_on_next_page;
				redraw  = MOTION;
			} break;
			case KEY_PPAGE:
			case '\002':
				if(first_on_page != 0)
					current = first_on_page - 1;
				else
					current = 0;
				redraw = MOTION;
				break;
			case '\n':
			case '\r':
			case KEY_ENTER:
			case ' ':
			case 'e':
				// edit prompt
				if(edit_url || c == 'e') {
					mvaddstr(LINES - 1, 0, gettext("URL: "));
					mmask_t oldmouse = 0;
					mousemask(0, &oldmouse);
					auto newurl = enter_string(url[current].url, url[current].urllen);
					if(newurl.len) {
						if(newurl.len != url[current].urllen || memcmp(url[current].url, newurl.data, newurl.len))
							redraw = INDEX;
						free(url[current].url);
						url[current].url    = newurl.data;
						url[current].urllen = newurl.len;
					}
					mousemask(oldmouse, NULL);
					move(LINES - 1, 0);
					clrtoeol();
					if(c == 'e' || !newurl.len)
						goto promptcancel;
				}
				endwin();


				char * execstatus    = NULL;
				size_t execstatuslen = 0;
#define EXECAPPENDONE(str, len_arg)                        \
	{                                                        \
		size_t len = (len_arg);                                \
		char * new = realloc(execstatus, execstatuslen + len); \
		if(new) {                                              \
			execstatus = new;                                    \
			memcpy(execstatus + execstatuslen, str, len);        \
			execstatuslen += len;                                \
		}                                                      \
	}
#define EXECAPPEND_PCNT_S(fmt, s, slen)                      \
	{                                                          \
		const char * pcnt = strstr(fmt, "%s");                   \
		EXECAPPENDONE(fmt, pcnt - fmt);                          \
		EXECAPPENDONE(s, slen == (size_t)-1 ? strlen(s) : slen); \
		EXECAPPENDONE(pcnt + 2, strlen(pcnt + 2));               \
	}

				bool okexit      = false;
				char * curcom    = command;
				size_t curcomlen = strcspn(curcom, ":");
				while(*curcom) {
					char *toexec, *tofree = NULL;
					size_t toexeclen = -1;
					if(via == ARGUMENT)
						toexec = tofree = quotesub(curcom, curcomlen, url[current].url, url[current].urllen);
					else {
						if(curcom[curcomlen])
							toexec = tofree = strndup(curcom, curcomlen);
						else
							toexec = curcom;
						toexeclen = curcomlen;
					}
					EXECAPPEND_PCNT_S(gettext("Executing: %s... "), toexec, toexeclen);
					printf(gettext("Executing: %s... "), toexec);
					fflush(stdout);
					int childret = system_via(toexec, url[current].url, via);
					free(tofree);
					if(WIFEXITED(childret) && !WEXITSTATUS(childret)) {
						EXECAPPENDONE("i", 1);
						putchar('\n');
						okexit = true;
						break;
					}

					char childrets_buf[11 + 1], *childrets = childrets_buf;  // -2147483648
					size_t childrets_len = -1;
					if(WIFSIGNALED(childret))
						childrets = strsignal(WTERMSIG(childret));
					else
						childrets_len = sprintf(childrets_buf, "%d", WEXITSTATUS(childret));

					// child exit code or human-readable signal name, written after "Executing: %s... "
					EXECAPPEND_PCNT_S(gettext("%s! "), childrets, childrets_len);
					printf(gettext("%s!\n"), childrets);

					curcom += curcomlen;
					if(*curcom)
						++curcom;
					curcomlen = strcspn(curcom, ":");
				}
				if(execstatus) {
					size_t off = 0;
					for(; off != execstatuslen && mvaddnstr(LINES - 1, 0, execstatus + off, execstatuslen - off) == ERR; ++off)
						;
					if(okexit) {
						mvaddch(LINES - 1, getcurx(stdscr) - 1, ACS_DIAMOND);
						clrtoeol();
					}
					free(execstatus);
				}
				redraw = MIN(redraw, CURSORPOS);
				break;
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9': {
				// edit prompt
				mvaddstr(LINES - 1, 0, gettext("Jump to URL: "));
				auto num = enter_string(&(char){c}, 1);
				if(num.len) {
					static_assert(sizeof(size_t) == sizeof(unsigned long), "size mismatch");
					char * end;
					size_t i = strtoul(num.data, &end, 0);
					if(i < 1 || i > urlcount || *end) {
						// error from the above
						mvaddstr(LINES - 1, 0, gettext("No such URL!"));
						redraw = CURSORPOS;
					} else {
						current = i - 1;
						move(LINES - 1, 0);
						redraw = MOTION;
					}
					free(num.data);
					clrtoeol();
				} else
					goto promptcancel;
			} break;
			case '\f':
			case '\007':
				clearok(stdscr, TRUE);
				redraw = FULL;
				break;
			case 'n':
			case 'N':
				c = c == 'n' ? '/' : '?';
				if(search_rx.re_nsub != (size_t)-1) {
					search_common(NULL, &search_rx, urlcount, url, &redraw, &current, c == '/' ? search_forward : search_backward);
					break;
				}
				__attribute__((fallthrough));
			case '/':
			case '?':
				mvaddstr(LINES - 1, 0, c == '/' ? /* edit prompt */ gettext("Search forwards for: ") : /* edit prompt */ gettext("Search backwards for: "));
				auto newsearch = enter_string(search.data, search.len);
				if(newsearch.len) {
					free(search.data);
					search = newsearch;
					search_common(search.data, &search_rx, urlcount, url, &redraw, &current, c == '/' ? search_forward : search_backward);
				} else {
				promptcancel:
					move(LINES - 1, 0);
					clrtoeol();
					redraw = MIN(redraw, CURSORPOS);
				}
				break;
		}
	}

	endwin();
	return error;
}
