#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/wait.h>

#include <errno.h>
#include <fcntl.h>
#include <locale.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>

#include "util.h"

/* curses */
#ifndef SFEED_MINICURSES
#include <curses.h>
#include <term.h>
#else
#include "minicurses.h"
#endif

#define LEN(a)   sizeof((a))/sizeof((a)[0])
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define MIN(a,b) ((a) < (b) ? (a) : (b))

#ifndef SFEED_DUMBTERM
#define SCROLLBAR_SYMBOL_BAR   "\xe2\x94\x82" /* symbol: "light vertical" */
#define SCROLLBAR_SYMBOL_TICK  " "
#define LINEBAR_SYMBOL_BAR     "\xe2\x94\x80" /* symbol: "light horizontal" */
#define LINEBAR_SYMBOL_RIGHT   "\xe2\x94\xa4" /* symbol: "light vertical and left" */
#else
#define SCROLLBAR_SYMBOL_BAR   "|"
#define SCROLLBAR_SYMBOL_TICK  " "
#define LINEBAR_SYMBOL_BAR     "-"
#define LINEBAR_SYMBOL_RIGHT   "|"
#endif

/* color-theme */
#ifndef SFEED_THEME
#define SFEED_THEME "themes/mono.h"
#endif
#include SFEED_THEME

enum {
	ATTR_RESET = 0,	ATTR_BOLD_ON = 1, ATTR_FAINT_ON = 2, ATTR_REVERSE_ON = 7
};

enum Layout {
	LayoutVertical = 0, LayoutHorizontal, LayoutMonocle, LayoutLast
};

enum Pane { PaneFeeds, PaneItems, PaneLast };

struct win {
	int width; /* absolute width of the window */
	int height; /* absolute height of the window */
	int dirty; /* needs draw update: clears screen */
};

struct row {
	char *text; /* text string, optional if using row_format() callback */
	int bold;
	void *data; /* data binding */
};

struct pane {
	int x; /* absolute x position on the screen */
	int y; /* absolute y position on the screen */
	int width; /* absolute width of the pane */
	int height; /* absolute height of the pane, should be > 0 */
	off_t pos; /* focused row position */
	struct row *rows;
	size_t nrows; /* total amount of rows */
	int focused; /* has focus or not */
	int hidden; /* is visible or not */
	int dirty; /* needs draw update */
	/* (optional) callback functions */
	struct row *(*row_get)(struct pane *, off_t);
	char *(*row_format)(struct pane *, struct row *);
	int (*row_match)(struct pane *, struct row *, const char *);
};

struct scrollbar {
	int tickpos;
	int ticksize;
	int x; /* absolute x position on the screen */
	int y; /* absolute y position on the screen */
	int size; /* absolute size of the bar, should be > 0 */
	int focused; /* has focus or not */
	int hidden; /* is visible or not */
	int dirty; /* needs draw update */
};

struct statusbar {
	int x; /* absolute x position on the screen */
	int y; /* absolute y position on the screen */
	int width; /* absolute width of the bar */
	char *text; /* data */
	int hidden; /* is visible or not */
	int dirty; /* needs draw update */
};

struct linebar {
	int x; /* absolute x position on the screen */
	int y; /* absolute y position on the screen */
	int width; /* absolute width of the line */
	int hidden; /* is visible or not */
	int dirty; /* needs draw update */
};

/* /UI */

struct item {
	char *fields[FieldLast];
	char *line; /* allocated split line */
	/* field to match new items, if link is set match on link, else on id */
	char *matchnew;
	time_t timestamp;
	int timeok;
	int isnew;
	off_t offset; /* line offset in file for lazyload */
};

struct urls {
	char **items; /* array of URLs */
	size_t len;   /* amount of items */
	size_t cap;   /* available capacity */
};

struct items {
	struct item *items; /* array of items */
	size_t len;         /* amount of items */
	size_t cap;         /* available capacity */
};

static void alldirty(void);
static void cleanup(void);
static void draw(void);
static int getsidebarsize(void);
static void markread(struct pane *, off_t, off_t, int);
static void pane_draw(struct pane *);
static void sighandler(int);
static void updategeom(void);
static void updatesidebar(void);
static void urls_free(struct urls *);
static int urls_hasmatch(struct urls *, const char *);
static void urls_read(struct urls *, const char *);

static struct linebar linebar;
static struct statusbar statusbar;
static struct pane panes[PaneLast];
static struct scrollbar scrollbars[PaneLast]; /* each pane has a scrollbar */
static struct win win;
static size_t selpane;
/* fixed sidebar size, < 0 is automatic */
static int fixedsidebarsizes[LayoutLast] = { -1, -1, -1 };
static int layout = LayoutVertical, prevlayout = LayoutVertical;
static int onlynew = 0; /* show only new in sidebar */
static int usemouse = 1; /* use xterm mouse tracking */

static struct termios tsave; /* terminal state at startup */
static struct termios tcur;
static int devnullfd;
static int istermsetup, needcleanup;

static struct feed *feeds;
static struct feed *curfeed;
static size_t nfeeds; /* amount of feeds */
static time_t comparetime;
static struct urls urls;
static char *urlfile;

volatile sig_atomic_t state_sigchld = 0, state_sighup = 0, state_sigint = 0;
volatile sig_atomic_t state_sigterm = 0, state_sigwinch = 0;

static char *plumbercmd = "xdg-open"; /* env variable: $SFEED_PLUMBER */
static char *pipercmd = "sfeed_content"; /* env variable: $SFEED_PIPER */
static char *yankercmd = "xclip -r"; /* env variable: $SFEED_YANKER */
static char *markreadcmd = "sfeed_markread read"; /* env variable: $SFEED_MARK_READ */
static char *markunreadcmd = "sfeed_markread unread"; /* env variable: $SFEED_MARK_UNREAD */
static char *cmdenv; /* env variable: $SFEED_AUTOCMD */
static int plumberia = 0; /* env variable: $SFEED_PLUMBER_INTERACTIVE */
static int piperia = 1; /* env variable: $SFEED_PIPER_INTERACTIVE */
static int yankeria = 0; /* env variable: $SFEED_YANKER_INTERACTIVE */
static int lazyload = 0; /* env variable: $SFEED_LAZYLOAD */

static int
ttywritef(const char *fmt, ...)
{
	va_list ap;
	int n;

	va_start(ap, fmt);
	n = vfprintf(stdout, fmt, ap);
	va_end(ap);
	fflush(stdout);

	return n;
}

static int
ttywrite(const char *s)
{
	if (!s)
		return 0; /* for tparm() returning NULL */
	return write(1, s, strlen(s));
}

/* Print to stderr, call cleanup() and _exit(). */
__dead static void
die(const char *fmt, ...)
{
	va_list ap;
	int saved_errno;

	saved_errno = errno;
	cleanup();

	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	va_end(ap);

	if (saved_errno)
		fprintf(stderr, ": %s", strerror(saved_errno));
	putc('\n', stderr);
	fflush(stderr);

	_exit(1);
}

static void *
erealloc(void *ptr, size_t size)
{
	void *p;

	if (!(p = realloc(ptr, size)))
		die("realloc");
	return p;
}

static void *
ecalloc(size_t nmemb, size_t size)
{
	void *p;

	if (!(p = calloc(nmemb, size)))
		die("calloc");
	return p;
}

static char *
estrdup(const char *s)
{
	char *p;

	if (!(p = strdup(s)))
		die("strdup");
	return p;
}

/* Wrapper for tparm() which allows NULL parameter for str. */
static char *
tparmnull(const char *str, long p1, long p2, long p3, long p4, long p5, long p6,
          long p7, long p8, long p9)
{
	if (!str)
		return NULL;
	/* some tparm() implementations have char *, some have const char * */
	return tparm((char *)str, p1, p2, p3, p4, p5, p6, p7, p8, p9);
}

/* Counts column width of character string. */
static size_t
colw(const char *s)
{
	wchar_t wc;
	size_t col = 0, i, slen;
	int inc, rl, w;

	slen = strlen(s);
	for (i = 0; i < slen; i += inc) {
		inc = 1; /* next byte */
		if ((unsigned char)s[i] < 32) {
			continue;
		} else if ((unsigned char)s[i] >= 127) {
			rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4);
			inc = rl;
			if (rl < 0) {
				mbtowc(NULL, NULL, 0); /* reset state */
				inc = 1; /* invalid, seek next byte */
				w = 1; /* replacement char is one width */
			} else if ((w = wcwidth(wc)) == -1) {
				continue;
			}
			col += w;
		} else {
			col++;
		}
	}
	return col;
}

/* Format `len` columns of characters. If string is shorter pad the rest
 * with characters `pad`. */
static int
utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad)
{
	wchar_t wc;
	size_t col = 0, i, slen, siz = 0;
	int inc, rl, w;

	if (!bufsiz)
		return -1;
	if (!len) {
		buf[0] = '\0';
		return 0;
	}

	slen = strlen(s);
	for (i = 0; i < slen; i += inc) {
		inc = 1; /* next byte */
		if ((unsigned char)s[i] < 32)
			continue;

		rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4);
		inc = rl;
		if (rl < 0) {
			mbtowc(NULL, NULL, 0); /* reset state */
			inc = 1; /* invalid, seek next byte */
			w = 1; /* replacement char is one width */
		} else if ((w = wcwidth(wc)) == -1) {
			continue;
		}

		if (col + w > len || (col + w == len && s[i + inc])) {
			if (siz + 4 >= bufsiz)
				return -1;
			memcpy(&buf[siz], PAD_TRUNCATE_SYMBOL, sizeof(PAD_TRUNCATE_SYMBOL) - 1);
			siz += sizeof(PAD_TRUNCATE_SYMBOL) - 1;
			buf[siz] = '\0';
			col++;
			break;
		} else if (rl < 0) {
			if (siz + 4 >= bufsiz)
				return -1;
			memcpy(&buf[siz], UTF_INVALID_SYMBOL, sizeof(UTF_INVALID_SYMBOL) - 1);
			siz += sizeof(UTF_INVALID_SYMBOL) - 1;
			buf[siz] = '\0';
			col++;
			continue;
		}
		if (siz + inc + 1 >= bufsiz)
			return -1;
		memcpy(&buf[siz], &s[i], inc);
		siz += inc;
		buf[siz] = '\0';
		col += w;
	}

	len -= col;
	if (siz + len + 1 >= bufsiz)
		return -1;
	memset(&buf[siz], pad, len);
	siz += len;
	buf[siz] = '\0';

	return 0;
}

static void
resetstate(void)
{
	ttywrite("\x1b""c"); /* rs1: reset title and state */
}

static void
updatetitle(void)
{
	unsigned long totalnew = 0, total = 0;
	size_t i;

	for (i = 0; i < nfeeds; i++) {
		totalnew += feeds[i].totalnew;
		total += feeds[i].total;
	}
	ttywritef("\x1b]2;(%lu/%lu) - sfeed_curses\x1b\\", totalnew, total);
}

static void
appmode(int on)
{
	ttywrite(tparmnull(on ? enter_ca_mode : exit_ca_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
}

static void
mousemode(int on)
{
	ttywrite(on ? "\x1b[?1000h" : "\x1b[?1000l"); /* xterm X10 mouse mode */
	ttywrite(on ? "\x1b[?1006h" : "\x1b[?1006l"); /* extended SGR mouse mode */
}

static void
cursormode(int on)
{
	ttywrite(tparmnull(on ? cursor_normal : cursor_invisible, 0, 0, 0, 0, 0, 0, 0, 0, 0));
}

static void
cursormove(int x, int y)
{
	ttywrite(tparmnull(cursor_address, y, x, 0, 0, 0, 0, 0, 0, 0));
}

static void
cursorsave(void)
{
	/* do not save the cursor if it won't be restored anyway */
	if (cursor_invisible)
		ttywrite(tparmnull(save_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0));
}

static void
cursorrestore(void)
{
	/* if the cursor cannot be hidden then move to a consistent position */
	if (cursor_invisible)
		ttywrite(tparmnull(restore_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0));
	else
		cursormove(0, 0);
}

static void
attrmode(int mode)
{
	switch (mode) {
	case ATTR_RESET:
		ttywrite(tparmnull(exit_attribute_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
		break;
	case ATTR_BOLD_ON:
		ttywrite(tparmnull(enter_bold_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
		break;
	case ATTR_FAINT_ON:
		ttywrite(tparmnull(enter_dim_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
		break;
	case ATTR_REVERSE_ON:
		ttywrite(tparmnull(enter_reverse_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
		break;
	default:
		break;
	}
}

static void
cleareol(void)
{
	ttywrite(tparmnull(clr_eol, 0, 0, 0, 0, 0, 0, 0, 0, 0));
}

static void
clearscreen(void)
{
	ttywrite(tparmnull(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0));
}

static void
cleanup(void)
{
	struct sigaction sa;

	if (!needcleanup)
		return;
	needcleanup = 0;

	if (istermsetup) {
		resetstate();
		cursormode(1);
		appmode(0);
		clearscreen();

		if (usemouse)
			mousemode(0);
	}

	/* restore terminal settings */
	tcsetattr(0, TCSANOW, &tsave);

	memset(&sa, 0, sizeof(sa));
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_RESTART; /* require BSD signal semantics */
	sa.sa_handler = SIG_DFL;
	sigaction(SIGWINCH, &sa, NULL);
}

static void
win_update(struct win *w, int width, int height)
{
	if (width != w->width || height != w->height)
		w->dirty = 1;
	w->width = width;
	w->height = height;
}

static void
resizewin(void)
{
	struct winsize winsz;
	int width, height;

	if (ioctl(1, TIOCGWINSZ, &winsz) != -1) {
		width = winsz.ws_col > 0 ? winsz.ws_col : 80;
		height = winsz.ws_row > 0 ? winsz.ws_row : 24;
		win_update(&win, width, height);
	}
	if (win.dirty)
		alldirty();
}

static void
init(void)
{
	struct sigaction sa;
	int errret = 1;

	needcleanup = 1;

	tcgetattr(0, &tsave);
	memcpy(&tcur, &tsave, sizeof(tcur));
	tcur.c_lflag &= ~(ECHO|ICANON);
	tcur.c_cc[VMIN] = 1;
	tcur.c_cc[VTIME] = 0;
	tcsetattr(0, TCSANOW, &tcur);

	if (!istermsetup &&
	    (setupterm(NULL, 1, &errret) != OK || errret != 1)) {
		errno = 0;
		die("setupterm: terminfo database or entry for $TERM not found");
	}
	istermsetup = 1;
	resizewin();

	appmode(1);
	cursormode(0);

	if (usemouse)
		mousemode(1);

	memset(&sa, 0, sizeof(sa));
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_RESTART; /* require BSD signal semantics */
	sa.sa_handler = sighandler;
	sigaction(SIGCHLD, &sa, NULL);
	sigaction(SIGHUP, &sa, NULL);
	sigaction(SIGINT, &sa, NULL);
	sigaction(SIGTERM, &sa, NULL);
	sigaction(SIGWINCH, &sa, NULL);
}

static void
processexit(pid_t pid, int interactive)
{
	struct sigaction sa;

	if (interactive) {
		memset(&sa, 0, sizeof(sa));
		sigemptyset(&sa.sa_mask);
		sa.sa_flags = SA_RESTART; /* require BSD signal semantics */

		/* ignore SIGINT (^C) in parent for interactive applications */
		sa.sa_handler = SIG_IGN;
		sigaction(SIGINT, &sa, NULL);

		sa.sa_flags = 0; /* SIGTERM: interrupt waitpid(), no SA_RESTART */
		sa.sa_handler = sighandler;
		sigaction(SIGTERM, &sa, NULL);

		/* wait for process to change state, ignore errors */
		waitpid(pid, NULL, 0);

		init();
		updatesidebar();
		updategeom();
		updatetitle();
	}
}

/* Pipe item line or item field to a program.
 * If `field` is -1 then pipe the TSV line, else a specified field.
 * if `interactive` is 1 then cleanup and restore the tty and wait on the
 * process.
 * if 0 then don't do that and also write stdout and stderr to /dev/null. */
static void
pipeitem(const char *cmd, struct item *item, int field, int interactive)
{
	FILE *fp;
	pid_t pid;
	int i, status;

	if (interactive)
		cleanup();

	switch ((pid = fork())) {
	case -1:
		die("fork");
	case 0:
		if (!interactive) {
			dup2(devnullfd, 1); /* stdout */
			dup2(devnullfd, 2); /* stderr */
		}

		errno = 0;
		if (!(fp = popen(cmd, "w")))
			die("popen: %s", cmd);
		if (field == -1) {
			for (i = 0; i < FieldLast; i++) {
				if (i)
					putc('\t', fp);
				fputs(item->fields[i], fp);
			}
		} else {
			fputs(item->fields[field], fp);
		}
		putc('\n', fp);
		status = pclose(fp);
		status = WIFEXITED(status) ? WEXITSTATUS(status) : 127;
		_exit(status);
	default:
		processexit(pid, interactive);
	}
}

static void
forkexec(char *argv[], int interactive)
{
	pid_t pid;

	if (interactive)
		cleanup();

	switch ((pid = fork())) {
	case -1:
		die("fork");
	case 0:
		if (!interactive) {
			dup2(devnullfd, 0); /* stdin */
			dup2(devnullfd, 1); /* stdout */
			dup2(devnullfd, 2); /* stderr */
		}
		if (execvp(argv[0], argv) == -1)
			_exit(1);
	default:
		processexit(pid, interactive);
	}
}

static struct row *
pane_row_get(struct pane *p, off_t pos)
{
	if (pos < 0 || pos >= p->nrows)
		return NULL;

	if (p->row_get)
		return p->row_get(p, pos);
	return p->rows + pos;
}

static char *
pane_row_text(struct pane *p, struct row *row)
{
	/* custom formatter */
	if (p->row_format)
		return p->row_format(p, row);
	return row->text;
}

static int
pane_row_match(struct pane *p, struct row *row, const char *s)
{
	if (p->row_match)
		return p->row_match(p, row, s);
	return (strcasestr(pane_row_text(p, row), s) != NULL);
}

static void
pane_row_draw(struct pane *p, off_t pos, int selected)
{
	struct row *row;

	if (p->hidden || !p->width || !p->height ||
	    p->x >= win.width || p->y + (pos % p->height) >= win.height)
		return;

	row = pane_row_get(p, pos);

	cursorsave();
	cursormove(p->x, p->y + (pos % p->height));

	if (p->focused)
		THEME_ITEM_FOCUS();
	else
		THEME_ITEM_NORMAL();
	if (row && row->bold)
		THEME_ITEM_BOLD();
	if (selected)
		THEME_ITEM_SELECTED();
	if (row) {
		printutf8pad(stdout, pane_row_text(p, row), p->width, ' ');
		fflush(stdout);
	} else {
		ttywritef("%-*.*s", p->width, p->width, "");
	}

	attrmode(ATTR_RESET);
	cursorrestore();
}

static void
pane_setpos(struct pane *p, off_t pos)
{
	if (pos < 0)
		pos = 0; /* clamp */
	if (!p->nrows)
		return; /* invalid */
	if (pos >= p->nrows)
		pos = p->nrows - 1; /* clamp */
	if (pos == p->pos)
		return; /* no change */

	/* is on different scroll region? mark whole pane dirty */
	if (((p->pos - (p->pos % p->height)) / p->height) !=
	    ((pos - (pos % p->height)) / p->height)) {
		p->dirty = 1;
	} else {
		/* only redraw the 2 dirty rows */
		pane_row_draw(p, p->pos, 0);
		pane_row_draw(p, pos, 1);
	}
	p->pos = pos;
}

static void
pane_scrollpage(struct pane *p, int pages)
{
	off_t pos;

	if (pages < 0) {
		pos = p->pos - (-pages * p->height);
		pos -= (p->pos % p->height);
		pos += p->height - 1;
		pane_setpos(p, pos);
	} else if (pages > 0) {
		pos = p->pos + (pages * p->height);
		if ((p->pos % p->height))
			pos -= (p->pos % p->height);
		pane_setpos(p, pos);
	}
}

static void
pane_scrolln(struct pane *p, int n)
{
	pane_setpos(p, p->pos + n);
}

static void
pane_setfocus(struct pane *p, int on)
{
	if (p->focused != on) {
		p->focused = on;
		p->dirty = 1;
	}
}

static void
pane_draw(struct pane *p)
{
	off_t pos, y;

	if (!p->dirty)
		return;
	p->dirty = 0;
	if (p->hidden || !p->width || !p->height)
		return;

	/* draw visible rows */
	pos = p->pos - (p->pos % p->height);
	for (y = 0; y < p->height; y++)
		pane_row_draw(p, y + pos, (y + pos) == p->pos);
}

static void
setlayout(int n)
{
	if (layout != LayoutMonocle)
		prevlayout = layout; /* previous non-monocle layout */
	layout = n;
}

static void
updategeom(void)
{
	int h, w, x = 0, y = 0;

	panes[PaneFeeds].hidden = layout == LayoutMonocle && (selpane != PaneFeeds);
	panes[PaneItems].hidden = layout == LayoutMonocle && (selpane != PaneItems);
	linebar.hidden = layout != LayoutHorizontal;

	w = win.width;
	/* always reserve space for statusbar */
	h = MAX(win.height - 1, 1);

	panes[PaneFeeds].x = x;
	panes[PaneFeeds].y = y;

	switch (layout) {
	case LayoutVertical:
		panes[PaneFeeds].width = getsidebarsize();

		x += panes[PaneFeeds].width;
		w -= panes[PaneFeeds].width;

		/* space for scrollbar if sidebar is visible */
		w--;
		x++;

		panes[PaneFeeds].height = MAX(h, 1);
		break;
	case LayoutHorizontal:
		panes[PaneFeeds].height = getsidebarsize();

		h -= panes[PaneFeeds].height;
		y += panes[PaneFeeds].height;

		linebar.x = 0;
		linebar.y = y;
		linebar.width = win.width;

		h--;
		y++;

		panes[PaneFeeds].width = MAX(w - 1, 0);
		break;
	case LayoutMonocle:
		panes[PaneFeeds].height = MAX(h, 1);
		panes[PaneFeeds].width = MAX(w - 1, 0);
		break;
	}

	panes[PaneItems].x = x;
	panes[PaneItems].y = y;
	panes[PaneItems].width = MAX(w - 1, 0);
	panes[PaneItems].height = MAX(h, 1);
	if (x >= win.width || y + 1 >= win.height)
		panes[PaneItems].hidden = 1;

	scrollbars[PaneFeeds].x = panes[PaneFeeds].x + panes[PaneFeeds].width;
	scrollbars[PaneFeeds].y = panes[PaneFeeds].y;
	scrollbars[PaneFeeds].size = panes[PaneFeeds].height;
	scrollbars[PaneFeeds].hidden = panes[PaneFeeds].hidden;

	scrollbars[PaneItems].x = panes[PaneItems].x + panes[PaneItems].width;
	scrollbars[PaneItems].y = panes[PaneItems].y;
	scrollbars[PaneItems].size = panes[PaneItems].height;
	scrollbars[PaneItems].hidden = panes[PaneItems].hidden;

	statusbar.width = win.width;
	statusbar.x = 0;
	statusbar.y = MAX(win.height - 1, 0);

	alldirty();
}

static void
scrollbar_setfocus(struct scrollbar *s, int on)
{
	if (s->focused != on) {
		s->focused = on;
		s->dirty = 1;
	}
}

static void
scrollbar_update(struct scrollbar *s, off_t pos, off_t nrows, int pageheight)
{
	int tickpos = 0, ticksize = 0;

	/* do not show a scrollbar if all items fit on the page */
	if (nrows > pageheight) {
		ticksize = s->size / ((double)nrows / (double)pageheight);
		if (ticksize == 0)
			ticksize = 1;

		tickpos = (pos / (double)nrows) * (double)s->size;

		/* fixup due to cell precision */
		if (pos + pageheight >= nrows ||
		    tickpos + ticksize >= s->size)
			tickpos = s->size - ticksize;
	}

	if (s->tickpos != tickpos || s->ticksize != ticksize)
		s->dirty = 1;
	s->tickpos = tickpos;
	s->ticksize = ticksize;
}

static void
scrollbar_draw(struct scrollbar *s)
{
	off_t y;

	if (!s->dirty)
		return;
	s->dirty = 0;
	if (s->hidden || !s->size || s->x >= win.width || s->y >= win.height)
		return;

	cursorsave();

	/* draw bar (not tick) */
	if (s->focused)
		THEME_SCROLLBAR_FOCUS();
	else
		THEME_SCROLLBAR_NORMAL();
	for (y = 0; y < s->size; y++) {
		if (y >= s->tickpos && y < s->tickpos + s->ticksize)
			continue; /* skip tick */
		cursormove(s->x, s->y + y);
		ttywrite(SCROLLBAR_SYMBOL_BAR);
	}

	/* draw tick */
	if (s->focused)
		THEME_SCROLLBAR_TICK_FOCUS();
	else
		THEME_SCROLLBAR_TICK_NORMAL();
	for (y = s->tickpos; y < s->size && y < s->tickpos + s->ticksize; y++) {
		cursormove(s->x, s->y + y);
		ttywrite(SCROLLBAR_SYMBOL_TICK);
	}

	attrmode(ATTR_RESET);
	cursorrestore();
}

static int
readch(void)
{
	unsigned char b;
	fd_set readfds;
	struct timeval tv;

	if (cmdenv && *cmdenv) {
		b = *(cmdenv++); /* $SFEED_AUTOCMD */
		return (int)b;
	}

	for (;;) {
		FD_ZERO(&readfds);
		FD_SET(0, &readfds);
		tv.tv_sec = 0;
		tv.tv_usec = 250000; /* 250ms */
		switch (select(1, &readfds, NULL, NULL, &tv)) {
		case -1:
			if (errno != EINTR)
				die("select");
			return -2; /* EINTR: like a signal */
		case 0:
			return -3; /* time-out */
		}

		switch (read(0, &b, 1)) {
		case -1: die("read");
		case 0: return EOF;
		default: return (int)b;
		}
	}
}

static char *
lineeditor(void)
{
	char *input = NULL;
	size_t cap = 0, nchars = 0;
	int ch;

	if (usemouse)
		mousemode(0);
	for (;;) {
		if (nchars + 2 >= cap) {
			cap = cap ? cap * 2 : 32;
			input = erealloc(input, cap);
		}

		ch = readch();
		if (ch == EOF || ch == '\r' || ch == '\n') {
			input[nchars] = '\0';
			break;
		} else if (ch == '\b' || ch == 0x7f) {
			if (!nchars)
				continue;
			input[--nchars] = '\0';
			ttywrite("\b \b"); /* back, blank, back */
		} else if (ch >= ' ') {
			input[nchars] = ch;
			input[nchars + 1] = '\0';
			ttywrite(&input[nchars]);
			nchars++;
		} else if (ch < 0) {
			if (state_sigchld) {
				state_sigchld = 0;
				/* wait on child processes so they don't become a zombie */
				while (waitpid((pid_t)-1, NULL, WNOHANG) > 0)
					;
			}
			if (state_sigint)
				state_sigint = 0; /* cancel prompt and don't handle this signal */
			else if (state_sighup || state_sigterm)
				; /* cancel prompt and handle these signals */
			else /* no signal, time-out or SIGCHLD or SIGWINCH */
				continue; /* do not cancel: process signal later */

			free(input);
			input = NULL;
			break; /* cancel prompt */
		}
	}
	if (usemouse)
		mousemode(1);
	return input;
}

static char *
uiprompt(int x, int y, char *fmt, ...)
{
	va_list ap;
	char *input, buf[32];

	va_start(ap, fmt);
	vsnprintf(buf, sizeof(buf), fmt, ap);
	va_end(ap);

	cursorsave();
	cursormove(x, y);
	THEME_INPUT_LABEL();
	ttywrite(buf);
	attrmode(ATTR_RESET);

	THEME_INPUT_NORMAL();
	cleareol();
	cursormode(1);
	cursormove(x + colw(buf) + 1, y);

	input = lineeditor();
	attrmode(ATTR_RESET);

	cursormode(0);
	cursorrestore();

	return input;
}

static void
linebar_draw(struct linebar *b)
{
	int i;

	if (!b->dirty)
		return;
	b->dirty = 0;
	if (b->hidden || !b->width)
		return;

	cursorsave();
	cursormove(b->x, b->y);
	THEME_LINEBAR();
	for (i = 0; i < b->width - 1; i++)
		ttywrite(LINEBAR_SYMBOL_BAR);
	ttywrite(LINEBAR_SYMBOL_RIGHT);
	attrmode(ATTR_RESET);
	cursorrestore();
}

static void
statusbar_draw(struct statusbar *s)
{
	if (!s->dirty)
		return;
	s->dirty = 0;
	if (s->hidden || !s->width || s->x >= win.width || s->y >= win.height)
		return;

	cursorsave();
	cursormove(s->x, s->y);
	THEME_STATUSBAR();
	/* terminals without xenl (eat newline glitch) mess up scrolling when
	 * using the last cell on the last line on the screen. */
	printutf8pad(stdout, s->text, s->width - (!eat_newline_glitch), ' ');
	fflush(stdout);
	attrmode(ATTR_RESET);
	cursorrestore();
}

static void
statusbar_update(struct statusbar *s, const char *text)
{
	if (s->text && !strcmp(s->text, text))
		return;

	free(s->text);
	s->text = estrdup(text);
	s->dirty = 1;
}

/* Line to item, modifies and splits line in-place. */
static int
linetoitem(char *line, struct item *item)
{
	char *fields[FieldLast];
	time_t parsedtime;

	item->line = line;
	parseline(line, fields);
	memcpy(item->fields, fields, sizeof(fields));
	if (urlfile)
		item->matchnew = estrdup(fields[fields[FieldLink][0] ? FieldLink : FieldId]);
	else
		item->matchnew = NULL;

	parsedtime = 0;
	if (!strtotime(fields[FieldUnixTimestamp], &parsedtime)) {
		item->timestamp = parsedtime;
		item->timeok = 1;
	} else {
		item->timestamp = 0;
		item->timeok = 0;
	}

	return 0;
}

static void
feed_items_free(struct items *items)
{
	size_t i;

	for (i = 0; i < items->len; i++) {
		free(items->items[i].line);
		free(items->items[i].matchnew);
	}
	free(items->items);
	items->items = NULL;
	items->len = 0;
	items->cap = 0;
}

static void
feed_items_get(struct feed *f, FILE *fp, struct items *itemsret)
{
	struct item *item, *items = NULL;
	char *line = NULL;
	size_t cap, i, linesize = 0, nitems;
	ssize_t linelen, n;
	off_t offset;

	cap = nitems = 0;
	offset = 0;
	for (i = 0; ; i++) {
		if (i + 1 >= cap) {
			cap = cap ? cap * 2 : 16;
			items = erealloc(items, cap * sizeof(struct item));
		}
		if ((n = linelen = getline(&line, &linesize, fp)) > 0) {
			item = &items[i];

			item->offset = offset;
			offset += linelen;

			if (line[linelen - 1] == '\n')
				line[--linelen] = '\0';

			if (lazyload && f->path) {
				linetoitem(line, item);

				/* data is ignored here, will be lazy-loaded later. */
				item->line = NULL;
				memset(item->fields, 0, sizeof(item->fields));
			} else {
				linetoitem(estrdup(line), item);
			}

			nitems++;
		}
		if (ferror(fp))
			die("getline: %s", f->name);
		if (n <= 0 || feof(fp))
			break;
	}
	itemsret->items = items;
	itemsret->len = nitems;
	itemsret->cap = cap;
	free(line);
}

static void
updatenewitems(struct feed *f)
{
	struct pane *p;
	struct row *row;
	struct item *item;
	size_t i;

	p = &panes[PaneItems];
	p->dirty = 1;
	f->totalnew = 0;
	for (i = 0; i < p->nrows; i++) {
		row = &(p->rows[i]); /* do not use pane_row_get() */
		item = row->data;
		if (urlfile)
			item->isnew = !urls_hasmatch(&urls, item->matchnew);
		else
			item->isnew = (item->timeok && item->timestamp >= comparetime);
		row->bold = item->isnew;
		f->totalnew += item->isnew;
	}
	f->total = p->nrows;
}

static void
feed_load(struct feed *f, FILE *fp)
{
	/* static, reuse local buffers */
	static struct items items;
	struct pane *p;
	size_t i;

	feed_items_free(&items);
	feed_items_get(f, fp, &items);
	p = &panes[PaneItems];
	p->pos = 0;
	p->nrows = items.len;
	free(p->rows);
	p->rows = ecalloc(sizeof(p->rows[0]), items.len + 1);
	for (i = 0; i < items.len; i++)
		p->rows[i].data = &(items.items[i]); /* do not use pane_row_get() */

	updatenewitems(f);
}

static void
feed_count(struct feed *f, FILE *fp)
{
	char *fields[FieldLast];
	char *line = NULL;
	size_t linesize = 0;
	ssize_t linelen;
	time_t parsedtime;

	f->totalnew = f->total = 0;
	while ((linelen = getline(&line, &linesize, fp)) > 0) {
		if (line[linelen - 1] == '\n')
			line[--linelen] = '\0';
		parseline(line, fields);

		if (urlfile) {
			f->totalnew += !urls_hasmatch(&urls, fields[fields[FieldLink][0] ? FieldLink : FieldId]);
		} else {
			parsedtime = 0;
			if (!strtotime(fields[FieldUnixTimestamp], &parsedtime))
				f->totalnew += (parsedtime >= comparetime);
		}
		f->total++;
	}
	if (ferror(fp))
		die("getline: %s", f->name);
	free(line);
}

static void
feed_setenv(struct feed *f)
{
	if (f && f->path)
		setenv("SFEED_FEED_PATH", f->path, 1);
	else
		unsetenv("SFEED_FEED_PATH");
}

/* Change feed, have one file open, reopen file if needed. */
static void
feeds_set(struct feed *f)
{
	if (curfeed) {
		if (curfeed->path && curfeed->fp) {
			fclose(curfeed->fp);
			curfeed->fp = NULL;
		}
	}

	if (f && f->path) {
		if (!f->fp && !(f->fp = fopen(f->path, "rb")))
			die("fopen: %s", f->path);
	}

	feed_setenv(f);

	curfeed = f;
}

static void
feeds_load(struct feed *feeds, size_t nfeeds)
{
	struct feed *f;
	size_t i;

	errno = 0;
	if ((comparetime = getcomparetime()) == (time_t)-1)
		die("getcomparetime");

	for (i = 0; i < nfeeds; i++) {
		f = &feeds[i];

		if (f->path) {
			if (f->fp) {
				if (fseek(f->fp, 0, SEEK_SET))
					die("fseek: %s", f->path);
			} else {
				if (!(f->fp = fopen(f->path, "rb")))
					die("fopen: %s", f->path);
			}
		}
		if (!f->fp) {
			/* reading from stdin, just recount new */
			if (f == curfeed)
				updatenewitems(f);
			continue;
		}

		/* load first items, because of first selection or stdin. */
		if (f == curfeed) {
			feed_load(f, f->fp);
		} else {
			feed_count(f, f->fp);
			if (f->path && f->fp) {
				fclose(f->fp);
				f->fp = NULL;
			}
		}
	}
}

/* find row position of the feed if visible, else return -1 */
static off_t
feeds_row_get(struct pane *p, struct feed *f)
{
	struct row *row;
	struct feed *fr;
	off_t pos;

	for (pos = 0; pos < p->nrows; pos++) {
		if (!(row = pane_row_get(p, pos)))
			continue;
		fr = row->data;
		if (!strcmp(fr->name, f->name))
			return pos;
	}
	return -1;
}

static void
feeds_reloadall(void)
{
	struct pane *p;
	struct feed *f = NULL;
	struct row *row;
	off_t pos;

	p = &panes[PaneFeeds];
	if ((row = pane_row_get(p, p->pos)))
		f = row->data;

	pos = panes[PaneItems].pos; /* store numeric item position */
	feeds_set(curfeed); /* close and reopen feed if possible */
	urls_read(&urls, urlfile);
	feeds_load(feeds, nfeeds);
	urls_free(&urls);
	/* restore numeric item position */
	pane_setpos(&panes[PaneItems], pos);
	updatesidebar();
	updatetitle();

	/* try to find the same feed in the pane */
	if (f && (pos = feeds_row_get(p, f)) != -1)
		pane_setpos(p, pos);
	else
		pane_setpos(p, 0);
}

static void
feed_open_selected(struct pane *p)
{
	struct feed *f;
	struct row *row;

	if (!(row = pane_row_get(p, p->pos)))
		return;
	f = row->data;
	feeds_set(f);
	urls_read(&urls, urlfile);
	if (f->fp)
		feed_load(f, f->fp);
	urls_free(&urls);
	/* redraw row: counts could be changed */
	updatesidebar();
	updatetitle();

	if (layout == LayoutMonocle) {
		selpane = PaneItems;
		updategeom();
	}
}

static void
feed_plumb_selected_item(struct pane *p, int field)
{
	struct row *row;
	struct item *item;
	char *cmd[3]; /* will have: { plumbercmd, arg, NULL } */

	if (!(row = pane_row_get(p, p->pos)))
		return;
	markread(p, p->pos, p->pos, 1);
	item = row->data;
	cmd[0] = plumbercmd;
	cmd[1] = item->fields[field]; /* set first argument for plumber */
	cmd[2] = NULL;
	forkexec(cmd, plumberia);
}

static void
feed_pipe_selected_item(struct pane *p)
{
	struct row *row;
	struct item *item;

	if (!(row = pane_row_get(p, p->pos)))
		return;
	item = row->data;
	markread(p, p->pos, p->pos, 1);
	pipeitem(pipercmd, item, -1, piperia);
}

static void
feed_yank_selected_item(struct pane *p, int field)
{
	struct row *row;
	struct item *item;

	if (!(row = pane_row_get(p, p->pos)))
		return;
	item = row->data;
	pipeitem(yankercmd, item, field, yankeria);
}

/* calculate optimal (default) size */
static int
getsidebarsizedefault(void)
{
	struct feed *feed;
	size_t i;
	int len, size;

	switch (layout) {
	case LayoutVertical:
		for (i = 0, size = 0; i < nfeeds; i++) {
			feed = &feeds[i];
			len = snprintf(NULL, 0, " (%lu/%lu)",
			               feed->totalnew, feed->total) +
				       colw(feed->name);
			if (len > size)
				size = len;

			if (onlynew && feed->totalnew == 0)
				continue;
		}
		return MAX(MIN(win.width - 1, size), 0);
	case LayoutHorizontal:
		for (i = 0, size = 0; i < nfeeds; i++) {
			feed = &feeds[i];
			if (onlynew && feed->totalnew == 0)
				continue;
			size++;
		}
		return MAX(MIN((win.height - 1) / 2, size), 1);
	}
	return 0;
}

static int
getsidebarsize(void)
{
	int size;

	if ((size = fixedsidebarsizes[layout]) < 0)
		size = getsidebarsizedefault();
	return size;
}

static void
adjustsidebarsize(int n)
{
	int size;

	if ((size = fixedsidebarsizes[layout]) < 0)
		size = getsidebarsizedefault();
	if (n > 0) {
		if ((layout == LayoutVertical && size + 1 < win.width) ||
		    (layout == LayoutHorizontal && size + 1 < win.height))
			size++;
	} else if (n < 0) {
		if ((layout == LayoutVertical && size > 0) ||
		    (layout == LayoutHorizontal && size > 1))
			size--;
	}

	if (size != fixedsidebarsizes[layout]) {
		fixedsidebarsizes[layout] = size;
		updategeom();
	}
}

static void
updatesidebar(void)
{
	struct pane *p;
	struct row *row;
	struct feed *feed;
	size_t i, nrows;
	int oldvalue = 0, newvalue = 0;

	p = &panes[PaneFeeds];
	if (!p->rows)
		p->rows = ecalloc(sizeof(p->rows[0]), nfeeds + 1);

	switch (layout) {
	case LayoutVertical:
		oldvalue = p->width;
		newvalue = getsidebarsize();
		p->width = newvalue;
		break;
	case LayoutHorizontal:
		oldvalue = p->height;
		newvalue = getsidebarsize();
		p->height = newvalue;
		break;
	}

	nrows = 0;
	for (i = 0; i < nfeeds; i++) {
		feed = &feeds[i];

		row = &(p->rows[nrows]);
		row->bold = (feed->totalnew > 0);
		row->data = feed;

		if (onlynew && feed->totalnew == 0)
			continue;

		nrows++;
	}
	p->nrows = nrows;

	if (oldvalue != newvalue)
		updategeom();
	else
		p->dirty = 1;

	if (!p->nrows)
		p->pos = 0;
	else if (p->pos >= p->nrows)
		p->pos = p->nrows - 1;
}

static void
sighandler(int signo)
{
	switch (signo) {
	case SIGCHLD:  state_sigchld = 1;  break;
	case SIGHUP:   state_sighup = 1;   break;
	case SIGINT:   state_sigint = 1;   break;
	case SIGTERM:  state_sigterm = 1;  break;
	case SIGWINCH: state_sigwinch = 1; break;
	}
}

static void
alldirty(void)
{
	win.dirty = 1;
	panes[PaneFeeds].dirty = 1;
	panes[PaneItems].dirty = 1;
	scrollbars[PaneFeeds].dirty = 1;
	scrollbars[PaneItems].dirty = 1;
	linebar.dirty = 1;
	statusbar.dirty = 1;
}

static void
draw(void)
{
	struct row *row;
	struct item *item;
	size_t i;

	if (win.dirty)
		win.dirty = 0;

	for (i = 0; i < LEN(panes); i++) {
		pane_setfocus(&panes[i], i == selpane);
		pane_draw(&panes[i]);

		/* each pane has a scrollbar */
		scrollbar_setfocus(&scrollbars[i], i == selpane);
		scrollbar_update(&scrollbars[i],
		                 panes[i].pos - (panes[i].pos % panes[i].height),
		                 panes[i].nrows, panes[i].height);
		scrollbar_draw(&scrollbars[i]);
	}

	linebar_draw(&linebar);

	/* if item selection text changed then update the status text */
	if ((row = pane_row_get(&panes[PaneItems], panes[PaneItems].pos))) {
		item = row->data;
		statusbar_update(&statusbar, item->fields[FieldLink]);
	} else {
		statusbar_update(&statusbar, "");
	}
	statusbar_draw(&statusbar);
}

static void
mousereport(int button, int release, int keymask, int x, int y)
{
	struct pane *p;
	size_t i;
	off_t pos;
	int changedpane, dblclick;

	if (!usemouse || release || button == -1)
		return;

	for (i = 0; i < LEN(panes); i++) {
		p = &panes[i];
		if (p->hidden || !p->width || !p->height)
			continue;

		/* these button actions are done regardless of the position */
		switch (button) {
		case 7: /* side-button: backward */
			if (selpane == PaneFeeds)
				return;
			selpane = PaneFeeds;
			if (layout == LayoutMonocle)
				updategeom();
			return;
		case 8: /* side-button: forward */
			if (selpane == PaneItems)
				return;
			selpane = PaneItems;
			if (layout == LayoutMonocle)
				updategeom();
			return;
		}

		/* check if mouse position is in pane or in its scrollbar */
		if (!(x >= p->x && x < p->x + p->width + (!scrollbars[i].hidden) &&
		      y >= p->y && y < p->y + p->height))
			continue;

		changedpane = (selpane != i);
		selpane = i;
		/* relative position on screen */
		pos = y - p->y + p->pos - (p->pos % p->height);
		dblclick = (pos == p->pos); /* clicking the already selected row */

		switch (button) {
		case 0: /* left-click */
			if (!p->nrows || pos >= p->nrows)
				break;
			pane_setpos(p, pos);
			if (i == PaneFeeds)
				feed_open_selected(&panes[PaneFeeds]);
			else if (i == PaneItems && dblclick && !changedpane)
				feed_plumb_selected_item(&panes[PaneItems], FieldLink);
			break;
		case 2: /* right-click */
			if (!p->nrows || pos >= p->nrows)
				break;
			pane_setpos(p, pos);
			if (i == PaneItems)
				feed_pipe_selected_item(&panes[PaneItems]);
			break;
		case 3: /* scroll up */
		case 4: /* scroll down */
			pane_scrollpage(p, button == 3 ? -1 : +1);
			break;
		}
		return; /* do not bubble events */
	}
}

/* Custom formatter for feed row. */
static char *
feed_row_format(struct pane *p, struct row *row)
{
	/* static, reuse local buffers */
	static char *bufw, *text;
	static size_t bufwsize, textsize;
	struct feed *feed;
	size_t needsize;
	char counts[128];
	int len, w;

	feed = row->data;

	/* align counts to the right and pad the rest with spaces */
	len = snprintf(counts, sizeof(counts), "(%lu/%lu)",
	               feed->totalnew, feed->total);
	if (len > p->width)
		w = p->width;
	else
		w = p->width - len;

	needsize = (w + 1) * 4;
	if (needsize > bufwsize) {
		bufw = erealloc(bufw, needsize);
		bufwsize = needsize;
	}

	needsize = bufwsize + sizeof(counts) + 1;
	if (needsize > textsize) {
		text = erealloc(text, needsize);
		textsize = needsize;
	}

	if (utf8pad(bufw, bufwsize, feed->name, w, ' ') != -1)
		snprintf(text, textsize, "%s%s", bufw, counts);
	else
		text[0] = '\0';

	return text;
}

static int
feed_row_match(struct pane *p, struct row *row, const char *s)
{
	struct feed *feed;

	feed = row->data;

	return (strcasestr(feed->name, s) != NULL);
}

static struct row *
item_row_get(struct pane *p, off_t pos)
{
	struct row *itemrow;
	struct item *item;
	struct feed *f;
	char *line = NULL;
	size_t linesize = 0;
	ssize_t linelen;

	itemrow = p->rows + pos;
	item = itemrow->data;

	f = curfeed;
	if (f && f->path && f->fp && !item->line) {
		if (fseek(f->fp, item->offset, SEEK_SET))
			die("fseek: %s", f->path);

		if ((linelen = getline(&line, &linesize, f->fp)) <= 0) {
			if (ferror(f->fp))
				die("getline: %s", f->path);
			free(line);
			return NULL;
		}

		if (line[linelen - 1] == '\n')
			line[--linelen] = '\0';

		linetoitem(estrdup(line), item);
		free(line);

		itemrow->data = item;
	}
	return itemrow;
}

/* Custom formatter for item row. */
static char *
item_row_format(struct pane *p, struct row *row)
{
	/* static, reuse local buffers */
	static char *text;
	static size_t textsize;
	struct item *item;
	struct tm tm;
	size_t needsize;

	item = row->data;

	needsize = strlen(item->fields[FieldTitle]) + 21;
	if (needsize > textsize) {
		text = erealloc(text, needsize);
		textsize = needsize;
	}

	if (item->timeok && localtime_r(&(item->timestamp), &tm)) {
		snprintf(text, textsize, "%c %04d-%02d-%02d %02d:%02d %s",
		         item->fields[FieldEnclosure][0] ? '@' : ' ',
		         tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
		         tm.tm_hour, tm.tm_min, item->fields[FieldTitle]);
	} else {
		snprintf(text, textsize, "%c                  %s",
		         item->fields[FieldEnclosure][0] ? '@' : ' ',
		         item->fields[FieldTitle]);
	}

	return text;
}

static void
markread(struct pane *p, off_t from, off_t to, int isread)
{
	struct row *row;
	struct item *item;
	FILE *fp;
	off_t i;
	const char *cmd;
	int isnew = !isread, pid, status = -1, visstart;

	if (!urlfile || !p->nrows)
		return;

	cmd = isread ? markreadcmd : markunreadcmd;

	switch ((pid = fork())) {
	case -1:
		die("fork");
	case 0:
		dup2(devnullfd, 1); /* stdout */
		dup2(devnullfd, 2); /* stderr */

		errno = 0;
		if (!(fp = popen(cmd, "w")))
			die("popen: %s", cmd);

		for (i = from; i <= to && i < p->nrows; i++) {
			/* do not use pane_row_get(): no need for lazyload */
			row = &(p->rows[i]);
			item = row->data;
			if (item->isnew != isnew) {
				fputs(item->matchnew, fp);
				putc('\n', fp);
			}
		}
		status = pclose(fp);
		status = WIFEXITED(status) ? WEXITSTATUS(status) : 127;
		_exit(status);
	default:
		/* waitpid() and block on process status change,
		 * fail if the exit status code was unavailable or non-zero */
		if (waitpid(pid, &status, 0) <= 0 || status)
			break;

		visstart = p->pos - (p->pos % p->height); /* visible start */
		for (i = from; i <= to && i < p->nrows; i++) {
			row = &(p->rows[i]);
			item = row->data;
			if (item->isnew == isnew)
				continue;

			row->bold = item->isnew = isnew;
			curfeed->totalnew += isnew ? 1 : -1;

			/* draw if visible on screen */
			if (i >= visstart && i < visstart + p->height)
				pane_row_draw(p, i, i == p->pos);
		}
		updatesidebar();
		updatetitle();
	}
}

static int
urls_cmp(const void *v1, const void *v2)
{
	return strcmp(*((char **)v1), *((char **)v2));
}

static void
urls_free(struct urls *urls)
{
	while (urls->len > 0) {
		urls->len--;
		free(urls->items[urls->len]);
	}
	free(urls->items);
	urls->items = NULL;
	urls->len = 0;
	urls->cap = 0;
}

static int
urls_hasmatch(struct urls *urls, const char *url)
{
	return (urls->len &&
	       bsearch(&url, urls->items, urls->len, sizeof(char *), urls_cmp));
}

static void
urls_read(struct urls *urls, const char *urlfile)
{
	FILE *fp;
	char *line = NULL;
	size_t linesiz = 0;
	ssize_t n;

	urls_free(urls);

	if (!urlfile)
		return;
	if (!(fp = fopen(urlfile, "rb")))
		die("fopen: %s", urlfile);

	while ((n = getline(&line, &linesiz, fp)) > 0) {
		if (line[n - 1] == '\n')
			line[--n] = '\0';
		if (urls->len + 1 >= urls->cap) {
			urls->cap = urls->cap ? urls->cap * 2 : 16;
			urls->items = erealloc(urls->items, urls->cap * sizeof(char *));
		}
		urls->items[urls->len++] = estrdup(line);
	}
	if (ferror(fp))
		die("getline: %s", urlfile);
	fclose(fp);
	free(line);

	if (urls->len > 0)
		qsort(urls->items, urls->len, sizeof(char *), urls_cmp);
}

int
main(int argc, char *argv[])
{
	struct pane *p;
	struct feed *f;
	struct row *row;
	char *name, *tmp;
	char *search = NULL; /* search text */
	int button, ch, fd, i, keymask, release, x, y;
	off_t pos;

#ifdef __OpenBSD__
	if (pledge("stdio rpath tty proc exec", NULL) == -1)
		die("pledge");
#endif

	setlocale(LC_CTYPE, "");

	if ((tmp = getenv("SFEED_PLUMBER")))
		plumbercmd = tmp;
	if ((tmp = getenv("SFEED_PIPER")))
		pipercmd = tmp;
	if ((tmp = getenv("SFEED_YANKER")))
		yankercmd = tmp;
	if ((tmp = getenv("SFEED_PLUMBER_INTERACTIVE")))
		plumberia = !strcmp(tmp, "1");
	if ((tmp = getenv("SFEED_PIPER_INTERACTIVE")))
		piperia = !strcmp(tmp, "1");
	if ((tmp = getenv("SFEED_YANKER_INTERACTIVE")))
		yankeria = !strcmp(tmp, "1");
	if ((tmp = getenv("SFEED_MARK_READ")))
		markreadcmd = tmp;
	if ((tmp = getenv("SFEED_MARK_UNREAD")))
		markunreadcmd = tmp;
	if ((tmp = getenv("SFEED_LAZYLOAD")))
		lazyload = !strcmp(tmp, "1");
	urlfile = getenv("SFEED_URL_FILE"); /* can be NULL */
	cmdenv = getenv("SFEED_AUTOCMD"); /* can be NULL */

	setlayout(argc <= 1 ? LayoutMonocle : LayoutVertical);
	selpane = layout == LayoutMonocle ? PaneItems : PaneFeeds;

	panes[PaneFeeds].row_format = feed_row_format;
	panes[PaneFeeds].row_match = feed_row_match;
	panes[PaneItems].row_format = item_row_format;
	if (lazyload)
		panes[PaneItems].row_get = item_row_get;

	feeds = ecalloc(argc, sizeof(struct feed));
	if (argc == 1) {
		nfeeds = 1;
		f = &feeds[0];
		f->name = "stdin";
		if (!(f->fp = fdopen(0, "rb")))
			die("fdopen");
	} else {
		for (i = 1; i < argc; i++) {
			f = &feeds[i - 1];
			f->path = argv[i];
			name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i];
			f->name = name;
		}
		nfeeds = argc - 1;
	}
	feeds_set(&feeds[0]);
	urls_read(&urls, urlfile);
	feeds_load(feeds, nfeeds);
	urls_free(&urls);

	if (!isatty(0)) {
		if ((fd = open("/dev/tty", O_RDONLY)) == -1)
			die("open: /dev/tty");
		if (dup2(fd, 0) == -1)
			die("dup2(%d, 0): /dev/tty -> stdin", fd);
		close(fd);
	}
	if (argc == 1)
		feeds[0].fp = NULL;

	if ((devnullfd = open("/dev/null", O_WRONLY)) == -1)
		die("open: /dev/null");

	init();
	updatesidebar();
	updategeom();
	updatetitle();
	draw();

	while (1) {
		if ((ch = readch()) < 0)
			goto event;
		switch (ch) {
		case '\x1b':
			if ((ch = readch()) < 0)
				goto event;
			if (ch != '[' && ch != 'O')
				continue; /* unhandled */
			if ((ch = readch()) < 0)
				goto event;
			switch (ch) {
			case 'M': /* mouse: X10 encoding */
				if ((ch = readch()) < 0)
					goto event;
				button = ch - 32;
				if ((ch = readch()) < 0)
					goto event;
				x = ch - 32;
				if ((ch = readch()) < 0)
					goto event;
				y = ch - 32;

				keymask = button & (4 | 8 | 16); /* shift, meta, ctrl */
				button &= ~keymask; /* unset key mask */

				/* button numbers (0 - 2) encoded in lowest 2 bits
				 * release does not indicate which button (so set to 0).
				 * Handle extended buttons like scrollwheels
				 * and side-buttons by each range. */
				release = 0;
				if (button == 3) {
					button = -1;
					release = 1;
				} else if (button >= 128) {
					button -= 121;
				} else if (button >= 64) {
					button -= 61;
				}
				mousereport(button, release, keymask, x - 1, y - 1);
				break;
			case '<': /* mouse: SGR encoding */
				for (button = 0; ; button *= 10, button += ch - '0') {
					if ((ch = readch()) < 0)
						goto event;
					else if (ch == ';')
						break;
				}
				for (x = 0; ; x *= 10, x += ch - '0') {
					if ((ch = readch()) < 0)
						goto event;
					else if (ch == ';')
						break;
				}
				for (y = 0; ; y *= 10, y += ch - '0') {
					if ((ch = readch()) < 0)
						goto event;
					else if (ch == 'm' || ch == 'M')
						break; /* release or press */
				}
				release = ch == 'm';
				keymask = button & (4 | 8 | 16); /* shift, meta, ctrl */
				button &= ~keymask; /* unset key mask */

				if (button >= 128)
					button -= 121;
				else if (button >= 64)
					button -= 61;

				mousereport(button, release, keymask, x - 1, y - 1);
				break;
			/* DEC/SUN: ESC O char, HP: ESC char or SCO: ESC [ char */
			case 'A': goto keyup;    /* arrow up */
			case 'B': goto keydown;  /* arrow down */
			case 'C': goto keyright; /* arrow right */
			case 'D': goto keyleft;  /* arrow left */
			case 'F': goto endpos;   /* end */
			case 'G': goto nextpage; /* page down */
			case 'H': goto startpos; /* home */
			case 'I': goto prevpage; /* page up */
			default:
				if (!(ch >= '0' && ch <= '9'))
					break;
				for (i = ch - '0'; ;) {
					if ((ch = readch()) < 0) {
						goto event;
					} else if (ch >= '0' && ch <= '9') {
						i = (i * 10) + (ch - '0');
						continue;
					} else if (ch == '~') { /* DEC: ESC [ num ~ */
						switch (i) {
						case 1: goto startpos; /* home */
						case 4: goto endpos;   /* end */
						case 5: goto prevpage; /* page up */
						case 6: goto nextpage; /* page down */
						case 7: goto startpos; /* home: urxvt */
						case 8: goto endpos;   /* end: urxvt */
						}
					} else if (ch == 'z') { /* SUN: ESC [ num z */
						switch (i) {
						case 214: goto startpos; /* home */
						case 216: goto prevpage; /* page up */
						case 220: goto endpos;   /* end */
						case 222: goto nextpage; /* page down */
						}
					}
					break;
				}
			}
			break;
keyup:
		case 'k':
			pane_scrolln(&panes[selpane], -1);
			break;
keydown:
		case 'j':
			pane_scrolln(&panes[selpane], +1);
			break;
keyleft:
		case 'h':
			if (selpane == PaneFeeds)
				break;
			selpane = PaneFeeds;
			if (layout == LayoutMonocle)
				updategeom();
			break;
keyright:
		case 'l':
			if (selpane == PaneItems)
				break;
			selpane = PaneItems;
			if (layout == LayoutMonocle)
				updategeom();
			break;
		case 'K':
			p = &panes[selpane];
			if (!p->nrows)
				break;
			for (pos = p->pos - 1; pos >= 0; pos--) {
				if ((row = pane_row_get(p, pos)) && row->bold) {
					pane_setpos(p, pos);
					break;
				}
			}
			break;
		case 'J':
			p = &panes[selpane];
			if (!p->nrows)
				break;
			for (pos = p->pos + 1; pos < p->nrows; pos++) {
				if ((row = pane_row_get(p, pos)) && row->bold) {
					pane_setpos(p, pos);
					break;
				}
			}
			break;
		case '\t':
			selpane = selpane == PaneFeeds ? PaneItems : PaneFeeds;
			if (layout == LayoutMonocle)
				updategeom();
			break;
startpos:
		case 'g':
			pane_setpos(&panes[selpane], 0);
			break;
endpos:
		case 'G':
			p = &panes[selpane];
			if (p->nrows)
				pane_setpos(p, p->nrows - 1);
			break;
prevpage:
		case 2: /* ^B */
			pane_scrollpage(&panes[selpane], -1);
			break;
nextpage:
		case ' ':
		case 6: /* ^F */
			pane_scrollpage(&panes[selpane], +1);
			break;
		case '[':
		case ']':
			pane_scrolln(&panes[PaneFeeds], ch == '[' ? -1 : +1);
			feed_open_selected(&panes[PaneFeeds]);
			break;
		case '/': /* new search (forward) */
		case '?': /* new search (backward) */
		case 'n': /* search again (forward) */
		case 'N': /* search again (backward) */
			p = &panes[selpane];

			/* prompt for new input */
			if (ch == '?' || ch == '/') {
				tmp = ch == '?' ? "backward" : "forward";
				free(search);
				search = uiprompt(statusbar.x, statusbar.y,
				                  "Search (%s):", tmp);
				statusbar.dirty = 1;
			}
			if (!search || !p->nrows)
				break;

			if (ch == '/' || ch == 'n') {
				/* forward */
				for (pos = p->pos + 1; pos < p->nrows; pos++) {
					if (pane_row_match(p, pane_row_get(p, pos), search)) {
						pane_setpos(p, pos);
						break;
					}
				}
			} else {
				/* backward */
				for (pos = p->pos - 1; pos >= 0; pos--) {
					if (pane_row_match(p, pane_row_get(p, pos), search)) {
						pane_setpos(p, pos);
						break;
					}
				}
			}
			break;
		case 12: /* ^L, redraw */
			alldirty();
			break;
		case 'R': /* reload all files */
			feeds_reloadall();
			break;
		case 'a': /* attachment */
		case 'e': /* enclosure */
		case '@':
			if (selpane == PaneItems)
				feed_plumb_selected_item(&panes[selpane], FieldEnclosure);
			break;
		case 'm': /* toggle mouse mode */
			usemouse = !usemouse;
			mousemode(usemouse);
			break;
		case '<': /* decrease fixed sidebar width */
		case '>': /* increase fixed sidebar width */
			adjustsidebarsize(ch == '<' ? -1 : +1);
			break;
		case '=': /* reset fixed sidebar to automatic size */
			fixedsidebarsizes[layout] = -1;
			updategeom();
			break;
		case 't': /* toggle showing only new in sidebar */
			p = &panes[PaneFeeds];
			if ((row = pane_row_get(p, p->pos)))
				f = row->data;
			else
				f = NULL;

			onlynew = !onlynew;
			updatesidebar();

			/* try to find the same feed in the pane */
			if (f && f->totalnew &&
			    (pos = feeds_row_get(p, f)) != -1)
				pane_setpos(p, pos);
			else
				pane_setpos(p, 0);
			break;
		case 'o': /* feeds: load, items: plumb URL */
		case '\n':
			if (selpane == PaneFeeds && panes[selpane].nrows)
				feed_open_selected(&panes[selpane]);
			else if (selpane == PaneItems && panes[selpane].nrows)
				feed_plumb_selected_item(&panes[selpane], FieldLink);
			break;
		case 'c': /* items: pipe TSV line to program */
		case 'p':
		case '|':
			if (selpane == PaneItems)
				feed_pipe_selected_item(&panes[selpane]);
			break;
		case 'y': /* yank: pipe TSV field to yank URL to clipboard */
		case 'E': /* yank: pipe TSV field to yank enclosure to clipboard */
			if (selpane == PaneItems)
				feed_yank_selected_item(&panes[selpane],
				                        ch == 'y' ? FieldLink : FieldEnclosure);
			break;
		case 'f': /* mark all read */
		case 'F': /* mark all unread */
			if (panes[PaneItems].nrows) {
				p = &panes[PaneItems];
				markread(p, 0, p->nrows - 1, ch == 'f');
			}
			break;
		case 'r': /* mark item as read */
		case 'u': /* mark item as unread */
			if (selpane == PaneItems && panes[selpane].nrows) {
				p = &panes[selpane];
				markread(p, p->pos, p->pos, ch == 'r');
				pane_scrolln(&panes[selpane], +1);
			}
			break;
		case 's': /* toggle layout between monocle or non-monocle */
			setlayout(layout == LayoutMonocle ? prevlayout : LayoutMonocle);
			updategeom();
			break;
		case '1': /* vertical layout */
		case '2': /* horizontal layout */
		case '3': /* monocle layout */
			setlayout(ch - '1');
			updategeom();
			break;
		case 4: /* EOT */
		case 'q': goto end;
		}
event:
		if (ch == EOF)
			goto end;
		else if (ch == -3 && !state_sigchld && !state_sighup &&
		         !state_sigint && !state_sigterm && !state_sigwinch)
			continue; /* just a time-out, nothing to do */

		/* handle signals in a particular order */
		if (state_sigchld) {
			state_sigchld = 0;
			/* wait on child processes so they don't become a zombie,
			 * do not block the parent process if there is no status,
			 * ignore errors */
			while (waitpid((pid_t)-1, NULL, WNOHANG) > 0)
				;
		}
		if (state_sigterm) {
			cleanup();
			_exit(128 + SIGTERM);
		}
		if (state_sigint) {
			cleanup();
			_exit(128 + SIGINT);
		}
		if (state_sighup) {
			state_sighup = 0;
			feeds_reloadall();
		}
		if (state_sigwinch) {
			state_sigwinch = 0;
			resizewin();
			updategeom();
		}

		draw();
	}
end:
	cleanup();

	return 0;
}
