sfeed

simple feed reader - forked from git.codemadness.org/sfeed
git clone git://src.gearsix.net/sfeed
Log | Files | Refs | Atom | README | LICENSE

sfeed_curses.c (52242B)


      1 #include <sys/ioctl.h>
      2 #include <sys/select.h>
      3 #include <sys/time.h>
      4 #include <sys/types.h>
      5 #include <sys/wait.h>
      6 
      7 #include <errno.h>
      8 #include <fcntl.h>
      9 #include <locale.h>
     10 #include <signal.h>
     11 #include <stdarg.h>
     12 #include <stdio.h>
     13 #include <stdlib.h>
     14 #include <string.h>
     15 #include <termios.h>
     16 #include <time.h>
     17 #include <unistd.h>
     18 #include <wchar.h>
     19 
     20 #include "util.h"
     21 
     22 /* curses */
     23 #ifndef SFEED_MINICURSES
     24 #include <curses.h>
     25 #include <term.h>
     26 #else
     27 #include "minicurses.h"
     28 #endif
     29 
     30 #define LEN(a)   sizeof((a))/sizeof((a)[0])
     31 #define MAX(a,b) ((a) > (b) ? (a) : (b))
     32 #define MIN(a,b) ((a) < (b) ? (a) : (b))
     33 
     34 #ifndef SFEED_DUMBTERM
     35 #define SCROLLBAR_SYMBOL_BAR   "\xe2\x94\x82" /* symbol: "light vertical" */
     36 #define SCROLLBAR_SYMBOL_TICK  " "
     37 #define LINEBAR_SYMBOL_BAR     "\xe2\x94\x80" /* symbol: "light horizontal" */
     38 #define LINEBAR_SYMBOL_RIGHT   "\xe2\x94\xa4" /* symbol: "light vertical and left" */
     39 #else
     40 #define SCROLLBAR_SYMBOL_BAR   "|"
     41 #define SCROLLBAR_SYMBOL_TICK  " "
     42 #define LINEBAR_SYMBOL_BAR     "-"
     43 #define LINEBAR_SYMBOL_RIGHT   "|"
     44 #endif
     45 
     46 /* color-theme */
     47 #ifndef SFEED_THEME
     48 #define SFEED_THEME "themes/mono.h"
     49 #endif
     50 #include SFEED_THEME
     51 
     52 enum {
     53 	ATTR_RESET = 0,	ATTR_BOLD_ON = 1, ATTR_FAINT_ON = 2, ATTR_REVERSE_ON = 7
     54 };
     55 
     56 enum Layout {
     57 	LayoutVertical = 0, LayoutHorizontal, LayoutMonocle, LayoutLast
     58 };
     59 
     60 enum Pane { PaneFeeds, PaneItems, PaneLast };
     61 
     62 struct win {
     63 	int width; /* absolute width of the window */
     64 	int height; /* absolute height of the window */
     65 	int dirty; /* needs draw update: clears screen */
     66 };
     67 
     68 struct row {
     69 	char *text; /* text string, optional if using row_format() callback */
     70 	int bold;
     71 	void *data; /* data binding */
     72 };
     73 
     74 struct pane {
     75 	int x; /* absolute x position on the screen */
     76 	int y; /* absolute y position on the screen */
     77 	int width; /* absolute width of the pane */
     78 	int height; /* absolute height of the pane, should be > 0 */
     79 	off_t pos; /* focused row position */
     80 	struct row *rows;
     81 	size_t nrows; /* total amount of rows */
     82 	int focused; /* has focus or not */
     83 	int hidden; /* is visible or not */
     84 	int dirty; /* needs draw update */
     85 	/* (optional) callback functions */
     86 	struct row *(*row_get)(struct pane *, off_t);
     87 	char *(*row_format)(struct pane *, struct row *);
     88 	int (*row_match)(struct pane *, struct row *, const char *);
     89 };
     90 
     91 struct scrollbar {
     92 	int tickpos;
     93 	int ticksize;
     94 	int x; /* absolute x position on the screen */
     95 	int y; /* absolute y position on the screen */
     96 	int size; /* absolute size of the bar, should be > 0 */
     97 	int focused; /* has focus or not */
     98 	int hidden; /* is visible or not */
     99 	int dirty; /* needs draw update */
    100 };
    101 
    102 struct statusbar {
    103 	int x; /* absolute x position on the screen */
    104 	int y; /* absolute y position on the screen */
    105 	int width; /* absolute width of the bar */
    106 	char *text; /* data */
    107 	int hidden; /* is visible or not */
    108 	int dirty; /* needs draw update */
    109 };
    110 
    111 struct linebar {
    112 	int x; /* absolute x position on the screen */
    113 	int y; /* absolute y position on the screen */
    114 	int width; /* absolute width of the line */
    115 	int hidden; /* is visible or not */
    116 	int dirty; /* needs draw update */
    117 };
    118 
    119 /* /UI */
    120 
    121 struct item {
    122 	char *fields[FieldLast];
    123 	char *line; /* allocated split line */
    124 	/* field to match new items, if link is set match on link, else on id */
    125 	char *matchnew;
    126 	time_t timestamp;
    127 	int timeok;
    128 	int isnew;
    129 	off_t offset; /* line offset in file for lazyload */
    130 };
    131 
    132 struct urls {
    133 	char **items; /* array of URLs */
    134 	size_t len;   /* amount of items */
    135 	size_t cap;   /* available capacity */
    136 };
    137 
    138 struct items {
    139 	struct item *items; /* array of items */
    140 	size_t len;         /* amount of items */
    141 	size_t cap;         /* available capacity */
    142 };
    143 
    144 void alldirty(void);
    145 void cleanup(void);
    146 void draw(void);
    147 int getsidebarsize(void);
    148 void markread(struct pane *, off_t, off_t, int);
    149 void pane_draw(struct pane *);
    150 void sighandler(int);
    151 void updategeom(void);
    152 void updatesidebar(void);
    153 void urls_free(struct urls *);
    154 int urls_hasmatch(struct urls *, const char *);
    155 void urls_read(struct urls *, const char *);
    156 
    157 static struct linebar linebar;
    158 static struct statusbar statusbar;
    159 static struct pane panes[PaneLast];
    160 static struct scrollbar scrollbars[PaneLast]; /* each pane has a scrollbar */
    161 static struct win win;
    162 static size_t selpane;
    163 /* fixed sidebar size, < 0 is automatic */
    164 static int fixedsidebarsizes[LayoutLast] = { -1, -1, -1 };
    165 static int layout = LayoutVertical, prevlayout = LayoutVertical;
    166 static int onlynew = 0; /* show only new in sidebar */
    167 static int usemouse = 1; /* use xterm mouse tracking */
    168 
    169 static struct termios tsave; /* terminal state at startup */
    170 static struct termios tcur;
    171 static int devnullfd;
    172 static int istermsetup, needcleanup;
    173 
    174 static struct feed *feeds;
    175 static struct feed *curfeed;
    176 static size_t nfeeds; /* amount of feeds */
    177 static time_t comparetime;
    178 struct urls urls;
    179 static char *urlfile;
    180 
    181 volatile sig_atomic_t state_sigchld = 0, state_sighup = 0, state_sigint = 0;
    182 volatile sig_atomic_t state_sigterm = 0, state_sigwinch = 0;
    183 
    184 static char *plumbercmd = "xdg-open"; /* env variable: $SFEED_PLUMBER */
    185 static char *pipercmd = "sfeed_content"; /* env variable: $SFEED_PIPER */
    186 static char *yankercmd = "xclip -r"; /* env variable: $SFEED_YANKER */
    187 static char *markreadcmd = "sfeed_markread read"; /* env variable: $SFEED_MARK_READ */
    188 static char *markunreadcmd = "sfeed_markread unread"; /* env variable: $SFEED_MARK_UNREAD */
    189 static char *cmdenv; /* env variable: $SFEED_AUTOCMD */
    190 static int plumberia = 0; /* env variable: $SFEED_PLUMBER_INTERACTIVE */
    191 static int piperia = 1; /* env variable: $SFEED_PIPER_INTERACTIVE */
    192 static int yankeria = 0; /* env variable: $SFEED_YANKER_INTERACTIVE */
    193 static int lazyload = 0; /* env variable: $SFEED_LAZYLOAD */
    194 
    195 int
    196 ttywritef(const char *fmt, ...)
    197 {
    198 	va_list ap;
    199 	int n;
    200 
    201 	va_start(ap, fmt);
    202 	n = vfprintf(stdout, fmt, ap);
    203 	va_end(ap);
    204 	fflush(stdout);
    205 
    206 	return n;
    207 }
    208 
    209 int
    210 ttywrite(const char *s)
    211 {
    212 	if (!s)
    213 		return 0; /* for tparm() returning NULL */
    214 	return write(1, s, strlen(s));
    215 }
    216 
    217 /* Print to stderr, call cleanup() and _exit(). */
    218 __dead void
    219 die(const char *fmt, ...)
    220 {
    221 	va_list ap;
    222 	int saved_errno;
    223 
    224 	saved_errno = errno;
    225 	cleanup();
    226 
    227 	va_start(ap, fmt);
    228 	vfprintf(stderr, fmt, ap);
    229 	va_end(ap);
    230 
    231 	if (saved_errno)
    232 		fprintf(stderr, ": %s", strerror(saved_errno));
    233 	putc('\n', stderr);
    234 	fflush(stderr);
    235 
    236 	_exit(1);
    237 }
    238 
    239 void *
    240 erealloc(void *ptr, size_t size)
    241 {
    242 	void *p;
    243 
    244 	if (!(p = realloc(ptr, size)))
    245 		die("realloc");
    246 	return p;
    247 }
    248 
    249 void *
    250 ecalloc(size_t nmemb, size_t size)
    251 {
    252 	void *p;
    253 
    254 	if (!(p = calloc(nmemb, size)))
    255 		die("calloc");
    256 	return p;
    257 }
    258 
    259 char *
    260 estrdup(const char *s)
    261 {
    262 	char *p;
    263 
    264 	if (!(p = strdup(s)))
    265 		die("strdup");
    266 	return p;
    267 }
    268 
    269 /* Wrapper for tparm() which allows NULL parameter for str. */
    270 char *
    271 tparmnull(const char *str, long p1, long p2, long p3, long p4, long p5, long p6,
    272           long p7, long p8, long p9)
    273 {
    274 	if (!str)
    275 		return NULL;
    276 	/* some tparm() implementations have char *, some have const char * */
    277 	return tparm((char *)str, p1, p2, p3, p4, p5, p6, p7, p8, p9);
    278 }
    279 
    280 /* Counts column width of character string. */
    281 size_t
    282 colw(const char *s)
    283 {
    284 	wchar_t wc;
    285 	size_t col = 0, i, slen;
    286 	int inc, rl, w;
    287 
    288 	slen = strlen(s);
    289 	for (i = 0; i < slen; i += inc) {
    290 		inc = 1; /* next byte */
    291 		if ((unsigned char)s[i] < 32) {
    292 			continue;
    293 		} else if ((unsigned char)s[i] >= 127) {
    294 			rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4);
    295 			inc = rl;
    296 			if (rl < 0) {
    297 				mbtowc(NULL, NULL, 0); /* reset state */
    298 				inc = 1; /* invalid, seek next byte */
    299 				w = 1; /* replacement char is one width */
    300 			} else if ((w = wcwidth(wc)) == -1) {
    301 				continue;
    302 			}
    303 			col += w;
    304 		} else {
    305 			col++;
    306 		}
    307 	}
    308 	return col;
    309 }
    310 
    311 /* Format `len` columns of characters. If string is shorter pad the rest
    312    with characters `pad`. */
    313 int
    314 utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad)
    315 {
    316 	wchar_t wc;
    317 	size_t col = 0, i, slen, siz = 0;
    318 	int inc, rl, w;
    319 
    320 	if (!bufsiz)
    321 		return -1;
    322 	if (!len) {
    323 		buf[0] = '\0';
    324 		return 0;
    325 	}
    326 
    327 	slen = strlen(s);
    328 	for (i = 0; i < slen; i += inc) {
    329 		inc = 1; /* next byte */
    330 		if ((unsigned char)s[i] < 32)
    331 			continue;
    332 
    333 		rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4);
    334 		inc = rl;
    335 		if (rl < 0) {
    336 			mbtowc(NULL, NULL, 0); /* reset state */
    337 			inc = 1; /* invalid, seek next byte */
    338 			w = 1; /* replacement char is one width */
    339 		} else if ((w = wcwidth(wc)) == -1) {
    340 			continue;
    341 		}
    342 
    343 		if (col + w > len || (col + w == len && s[i + inc])) {
    344 			if (siz + 4 >= bufsiz)
    345 				return -1;
    346 			memcpy(&buf[siz], PAD_TRUNCATE_SYMBOL, sizeof(PAD_TRUNCATE_SYMBOL) - 1);
    347 			siz += sizeof(PAD_TRUNCATE_SYMBOL) - 1;
    348 			buf[siz] = '\0';
    349 			col++;
    350 			break;
    351 		} else if (rl < 0) {
    352 			if (siz + 4 >= bufsiz)
    353 				return -1;
    354 			memcpy(&buf[siz], UTF_INVALID_SYMBOL, sizeof(UTF_INVALID_SYMBOL) - 1);
    355 			siz += sizeof(UTF_INVALID_SYMBOL) - 1;
    356 			buf[siz] = '\0';
    357 			col++;
    358 			continue;
    359 		}
    360 		if (siz + inc + 1 >= bufsiz)
    361 			return -1;
    362 		memcpy(&buf[siz], &s[i], inc);
    363 		siz += inc;
    364 		buf[siz] = '\0';
    365 		col += w;
    366 	}
    367 
    368 	len -= col;
    369 	if (siz + len + 1 >= bufsiz)
    370 		return -1;
    371 	memset(&buf[siz], pad, len);
    372 	siz += len;
    373 	buf[siz] = '\0';
    374 
    375 	return 0;
    376 }
    377 
    378 void
    379 resetstate(void)
    380 {
    381 	ttywrite("\x1b""c"); /* rs1: reset title and state */
    382 }
    383 
    384 void
    385 updatetitle(void)
    386 {
    387 	unsigned long totalnew = 0, total = 0;
    388 	size_t i;
    389 
    390 	for (i = 0; i < nfeeds; i++) {
    391 		totalnew += feeds[i].totalnew;
    392 		total += feeds[i].total;
    393 	}
    394 	ttywritef("\x1b]2;(%lu/%lu) - sfeed_curses\x1b\\", totalnew, total);
    395 }
    396 
    397 void
    398 appmode(int on)
    399 {
    400 	ttywrite(tparmnull(on ? enter_ca_mode : exit_ca_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
    401 }
    402 
    403 void
    404 mousemode(int on)
    405 {
    406 	ttywrite(on ? "\x1b[?1000h" : "\x1b[?1000l"); /* xterm X10 mouse mode */
    407 	ttywrite(on ? "\x1b[?1006h" : "\x1b[?1006l"); /* extended SGR mouse mode */
    408 }
    409 
    410 void
    411 cursormode(int on)
    412 {
    413 	ttywrite(tparmnull(on ? cursor_normal : cursor_invisible, 0, 0, 0, 0, 0, 0, 0, 0, 0));
    414 }
    415 
    416 void
    417 cursormove(int x, int y)
    418 {
    419 	ttywrite(tparmnull(cursor_address, y, x, 0, 0, 0, 0, 0, 0, 0));
    420 }
    421 
    422 void
    423 cursorsave(void)
    424 {
    425 	/* do not save the cursor if it won't be restored anyway */
    426 	if (cursor_invisible)
    427 		ttywrite(tparmnull(save_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0));
    428 }
    429 
    430 void
    431 cursorrestore(void)
    432 {
    433 	/* if the cursor cannot be hidden then move to a consistent position */
    434 	if (cursor_invisible)
    435 		ttywrite(tparmnull(restore_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0));
    436 	else
    437 		cursormove(0, 0);
    438 }
    439 
    440 void
    441 attrmode(int mode)
    442 {
    443 	switch (mode) {
    444 	case ATTR_RESET:
    445 		ttywrite(tparmnull(exit_attribute_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
    446 		break;
    447 	case ATTR_BOLD_ON:
    448 		ttywrite(tparmnull(enter_bold_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
    449 		break;
    450 	case ATTR_FAINT_ON:
    451 		ttywrite(tparmnull(enter_dim_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
    452 		break;
    453 	case ATTR_REVERSE_ON:
    454 		ttywrite(tparmnull(enter_reverse_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
    455 		break;
    456 	default:
    457 		break;
    458 	}
    459 }
    460 
    461 void
    462 cleareol(void)
    463 {
    464 	ttywrite(tparmnull(clr_eol, 0, 0, 0, 0, 0, 0, 0, 0, 0));
    465 }
    466 
    467 void
    468 clearscreen(void)
    469 {
    470 	ttywrite(tparmnull(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0));
    471 }
    472 
    473 void
    474 cleanup(void)
    475 {
    476 	struct sigaction sa;
    477 
    478 	if (!needcleanup)
    479 		return;
    480 	needcleanup = 0;
    481 
    482 	if (istermsetup) {
    483 		resetstate();
    484 		cursormode(1);
    485 		appmode(0);
    486 		clearscreen();
    487 
    488 		if (usemouse)
    489 			mousemode(0);
    490 	}
    491 
    492 	/* restore terminal settings */
    493 	tcsetattr(0, TCSANOW, &tsave);
    494 
    495 	memset(&sa, 0, sizeof(sa));
    496 	sigemptyset(&sa.sa_mask);
    497 	sa.sa_flags = SA_RESTART; /* require BSD signal semantics */
    498 	sa.sa_handler = SIG_DFL;
    499 	sigaction(SIGWINCH, &sa, NULL);
    500 }
    501 
    502 void
    503 win_update(struct win *w, int width, int height)
    504 {
    505 	if (width != w->width || height != w->height)
    506 		w->dirty = 1;
    507 	w->width = width;
    508 	w->height = height;
    509 }
    510 
    511 void
    512 resizewin(void)
    513 {
    514 	struct winsize winsz;
    515 	int width, height;
    516 
    517 	if (ioctl(1, TIOCGWINSZ, &winsz) != -1) {
    518 		width = winsz.ws_col > 0 ? winsz.ws_col : 80;
    519 		height = winsz.ws_row > 0 ? winsz.ws_row : 24;
    520 		win_update(&win, width, height);
    521 	}
    522 	if (win.dirty)
    523 		alldirty();
    524 }
    525 
    526 void
    527 init(void)
    528 {
    529 	struct sigaction sa;
    530 	int errret = 1;
    531 
    532 	needcleanup = 1;
    533 
    534 	tcgetattr(0, &tsave);
    535 	memcpy(&tcur, &tsave, sizeof(tcur));
    536 	tcur.c_lflag &= ~(ECHO|ICANON);
    537 	tcur.c_cc[VMIN] = 1;
    538 	tcur.c_cc[VTIME] = 0;
    539 	tcsetattr(0, TCSANOW, &tcur);
    540 
    541 	if (!istermsetup &&
    542 	    (setupterm(NULL, 1, &errret) != OK || errret != 1)) {
    543 		errno = 0;
    544 		die("setupterm: terminfo database or entry for $TERM not found");
    545 	}
    546 	istermsetup = 1;
    547 	resizewin();
    548 
    549 	appmode(1);
    550 	cursormode(0);
    551 
    552 	if (usemouse)
    553 		mousemode(1);
    554 
    555 	memset(&sa, 0, sizeof(sa));
    556 	sigemptyset(&sa.sa_mask);
    557 	sa.sa_flags = SA_RESTART; /* require BSD signal semantics */
    558 	sa.sa_handler = sighandler;
    559 	sigaction(SIGCHLD, &sa, NULL);
    560 	sigaction(SIGHUP, &sa, NULL);
    561 	sigaction(SIGINT, &sa, NULL);
    562 	sigaction(SIGTERM, &sa, NULL);
    563 	sigaction(SIGWINCH, &sa, NULL);
    564 }
    565 
    566 void
    567 processexit(pid_t pid, int interactive)
    568 {
    569 	struct sigaction sa;
    570 
    571 	if (interactive) {
    572 		memset(&sa, 0, sizeof(sa));
    573 		sigemptyset(&sa.sa_mask);
    574 		sa.sa_flags = SA_RESTART; /* require BSD signal semantics */
    575 
    576 		/* ignore SIGINT (^C) in parent for interactive applications */
    577 		sa.sa_handler = SIG_IGN;
    578 		sigaction(SIGINT, &sa, NULL);
    579 
    580 		sa.sa_flags = 0; /* SIGTERM: interrupt waitpid(), no SA_RESTART */
    581 		sa.sa_handler = sighandler;
    582 		sigaction(SIGTERM, &sa, NULL);
    583 
    584 		/* wait for process to change state, ignore errors */
    585 		waitpid(pid, NULL, 0);
    586 
    587 		init();
    588 		updatesidebar();
    589 		updategeom();
    590 		updatetitle();
    591 	}
    592 }
    593 
    594 /* Pipe item line or item field to a program.
    595    If `field` is -1 then pipe the TSV line, else a specified field.
    596    if `interactive` is 1 then cleanup and restore the tty and wait on the
    597    process.
    598    if 0 then don't do that and also write stdout and stderr to /dev/null. */
    599 void
    600 pipeitem(const char *cmd, struct item *item, int field, int interactive)
    601 {
    602 	FILE *fp;
    603 	pid_t pid;
    604 	int i, status;
    605 
    606 	if (interactive)
    607 		cleanup();
    608 
    609 	switch ((pid = fork())) {
    610 	case -1:
    611 		die("fork");
    612 	case 0:
    613 		if (!interactive) {
    614 			dup2(devnullfd, 1); /* stdout */
    615 			dup2(devnullfd, 2); /* stderr */
    616 		}
    617 
    618 		errno = 0;
    619 		if (!(fp = popen(cmd, "w")))
    620 			die("popen: %s", cmd);
    621 		if (field == -1) {
    622 			for (i = 0; i < FieldLast; i++) {
    623 				if (i)
    624 					putc('\t', fp);
    625 				fputs(item->fields[i], fp);
    626 			}
    627 		} else {
    628 			fputs(item->fields[field], fp);
    629 		}
    630 		putc('\n', fp);
    631 		status = pclose(fp);
    632 		status = WIFEXITED(status) ? WEXITSTATUS(status) : 127;
    633 		_exit(status);
    634 	default:
    635 		processexit(pid, interactive);
    636 	}
    637 }
    638 
    639 void
    640 forkexec(char *argv[], int interactive)
    641 {
    642 	pid_t pid;
    643 
    644 	if (interactive)
    645 		cleanup();
    646 
    647 	switch ((pid = fork())) {
    648 	case -1:
    649 		die("fork");
    650 	case 0:
    651 		if (!interactive) {
    652 			dup2(devnullfd, 0); /* stdin */
    653 			dup2(devnullfd, 1); /* stdout */
    654 			dup2(devnullfd, 2); /* stderr */
    655 		}
    656 		if (execvp(argv[0], argv) == -1)
    657 			_exit(1);
    658 	default:
    659 		processexit(pid, interactive);
    660 	}
    661 }
    662 
    663 struct row *
    664 pane_row_get(struct pane *p, off_t pos)
    665 {
    666 	if (pos < 0 || pos >= p->nrows)
    667 		return NULL;
    668 
    669 	if (p->row_get)
    670 		return p->row_get(p, pos);
    671 	return p->rows + pos;
    672 }
    673 
    674 char *
    675 pane_row_text(struct pane *p, struct row *row)
    676 {
    677 	/* custom formatter */
    678 	if (p->row_format)
    679 		return p->row_format(p, row);
    680 	return row->text;
    681 }
    682 
    683 int
    684 pane_row_match(struct pane *p, struct row *row, const char *s)
    685 {
    686 	if (p->row_match)
    687 		return p->row_match(p, row, s);
    688 	return (strcasestr(pane_row_text(p, row), s) != NULL);
    689 }
    690 
    691 void
    692 pane_row_draw(struct pane *p, off_t pos, int selected)
    693 {
    694 	struct row *row;
    695 
    696 	if (p->hidden || !p->width || !p->height ||
    697 	    p->x >= win.width || p->y + (pos % p->height) >= win.height)
    698 		return;
    699 
    700 	row = pane_row_get(p, pos);
    701 
    702 	cursorsave();
    703 	cursormove(p->x, p->y + (pos % p->height));
    704 
    705 	if (p->focused)
    706 		THEME_ITEM_FOCUS();
    707 	else
    708 		THEME_ITEM_NORMAL();
    709 	if (row && row->bold)
    710 		THEME_ITEM_BOLD();
    711 	if (selected)
    712 		THEME_ITEM_SELECTED();
    713 	if (row) {
    714 		printutf8pad(stdout, pane_row_text(p, row), p->width, ' ');
    715 		fflush(stdout);
    716 	} else {
    717 		ttywritef("%-*.*s", p->width, p->width, "");
    718 	}
    719 
    720 	attrmode(ATTR_RESET);
    721 	cursorrestore();
    722 }
    723 
    724 void
    725 pane_setpos(struct pane *p, off_t pos)
    726 {
    727 	if (pos < 0)
    728 		pos = 0; /* clamp */
    729 	if (!p->nrows)
    730 		return; /* invalid */
    731 	if (pos >= p->nrows)
    732 		pos = p->nrows - 1; /* clamp */
    733 	if (pos == p->pos)
    734 		return; /* no change */
    735 
    736 	/* is on different scroll region? mark whole pane dirty */
    737 	if (((p->pos - (p->pos % p->height)) / p->height) !=
    738 	    ((pos - (pos % p->height)) / p->height)) {
    739 		p->dirty = 1;
    740 	} else {
    741 		/* only redraw the 2 dirty rows */
    742 		pane_row_draw(p, p->pos, 0);
    743 		pane_row_draw(p, pos, 1);
    744 	}
    745 	p->pos = pos;
    746 }
    747 
    748 void
    749 pane_scrollpage(struct pane *p, int pages)
    750 {
    751 	off_t pos;
    752 
    753 	if (pages < 0) {
    754 		pos = p->pos - (-pages * p->height);
    755 		pos -= (p->pos % p->height);
    756 		pos += p->height - 1;
    757 		pane_setpos(p, pos);
    758 	} else if (pages > 0) {
    759 		pos = p->pos + (pages * p->height);
    760 		if ((p->pos % p->height))
    761 			pos -= (p->pos % p->height);
    762 		pane_setpos(p, pos);
    763 	}
    764 }
    765 
    766 void
    767 pane_scrolln(struct pane *p, int n)
    768 {
    769 	pane_setpos(p, p->pos + n);
    770 }
    771 
    772 void
    773 pane_setfocus(struct pane *p, int on)
    774 {
    775 	if (p->focused != on) {
    776 		p->focused = on;
    777 		p->dirty = 1;
    778 	}
    779 }
    780 
    781 void
    782 pane_draw(struct pane *p)
    783 {
    784 	off_t pos, y;
    785 
    786 	if (!p->dirty)
    787 		return;
    788 	p->dirty = 0;
    789 	if (p->hidden || !p->width || !p->height)
    790 		return;
    791 
    792 	/* draw visible rows */
    793 	pos = p->pos - (p->pos % p->height);
    794 	for (y = 0; y < p->height; y++)
    795 		pane_row_draw(p, y + pos, (y + pos) == p->pos);
    796 }
    797 
    798 void
    799 setlayout(int n)
    800 {
    801 	if (layout != LayoutMonocle)
    802 		prevlayout = layout; /* previous non-monocle layout */
    803 	layout = n;
    804 }
    805 
    806 void
    807 updategeom(void)
    808 {
    809 	int h, w, x = 0, y = 0;
    810 
    811 	panes[PaneFeeds].hidden = layout == LayoutMonocle && (selpane != PaneFeeds);
    812 	panes[PaneItems].hidden = layout == LayoutMonocle && (selpane != PaneItems);
    813 	linebar.hidden = layout != LayoutHorizontal;
    814 
    815 	w = win.width;
    816 	/* always reserve space for statusbar */
    817 	h = MAX(win.height - 1, 1);
    818 
    819 	panes[PaneFeeds].x = x;
    820 	panes[PaneFeeds].y = y;
    821 
    822 	switch (layout) {
    823 	case LayoutVertical:
    824 		panes[PaneFeeds].width = getsidebarsize();
    825 
    826 		x += panes[PaneFeeds].width;
    827 		w -= panes[PaneFeeds].width;
    828 
    829 		/* space for scrollbar if sidebar is visible */
    830 		w--;
    831 		x++;
    832 
    833 		panes[PaneFeeds].height = MAX(h, 1);
    834 		break;
    835 	case LayoutHorizontal:
    836 		panes[PaneFeeds].height = getsidebarsize();
    837 
    838 		h -= panes[PaneFeeds].height;
    839 		y += panes[PaneFeeds].height;
    840 
    841 		linebar.x = 0;
    842 		linebar.y = y;
    843 		linebar.width = win.width;
    844 
    845 		h--;
    846 		y++;
    847 
    848 		panes[PaneFeeds].width = MAX(w - 1, 0);
    849 		break;
    850 	case LayoutMonocle:
    851 		panes[PaneFeeds].height = MAX(h, 1);
    852 		panes[PaneFeeds].width = MAX(w - 1, 0);
    853 		break;
    854 	}
    855 
    856 	panes[PaneItems].x = x;
    857 	panes[PaneItems].y = y;
    858 	panes[PaneItems].width = MAX(w - 1, 0);
    859 	panes[PaneItems].height = MAX(h, 1);
    860 	if (x >= win.width || y + 1 >= win.height)
    861 		panes[PaneItems].hidden = 1;
    862 
    863 	scrollbars[PaneFeeds].x = panes[PaneFeeds].x + panes[PaneFeeds].width;
    864 	scrollbars[PaneFeeds].y = panes[PaneFeeds].y;
    865 	scrollbars[PaneFeeds].size = panes[PaneFeeds].height;
    866 	scrollbars[PaneFeeds].hidden = panes[PaneFeeds].hidden;
    867 
    868 	scrollbars[PaneItems].x = panes[PaneItems].x + panes[PaneItems].width;
    869 	scrollbars[PaneItems].y = panes[PaneItems].y;
    870 	scrollbars[PaneItems].size = panes[PaneItems].height;
    871 	scrollbars[PaneItems].hidden = panes[PaneItems].hidden;
    872 
    873 	statusbar.width = win.width;
    874 	statusbar.x = 0;
    875 	statusbar.y = MAX(win.height - 1, 0);
    876 
    877 	alldirty();
    878 }
    879 
    880 void
    881 scrollbar_setfocus(struct scrollbar *s, int on)
    882 {
    883 	if (s->focused != on) {
    884 		s->focused = on;
    885 		s->dirty = 1;
    886 	}
    887 }
    888 
    889 void
    890 scrollbar_update(struct scrollbar *s, off_t pos, off_t nrows, int pageheight)
    891 {
    892 	int tickpos = 0, ticksize = 0;
    893 
    894 	/* do not show a scrollbar if all items fit on the page */
    895 	if (nrows > pageheight) {
    896 		ticksize = s->size / ((double)nrows / (double)pageheight);
    897 		if (ticksize == 0)
    898 			ticksize = 1;
    899 
    900 		tickpos = (pos / (double)nrows) * (double)s->size;
    901 
    902 		/* fixup due to cell precision */
    903 		if (pos + pageheight >= nrows ||
    904 		    tickpos + ticksize >= s->size)
    905 			tickpos = s->size - ticksize;
    906 	}
    907 
    908 	if (s->tickpos != tickpos || s->ticksize != ticksize)
    909 		s->dirty = 1;
    910 	s->tickpos = tickpos;
    911 	s->ticksize = ticksize;
    912 }
    913 
    914 void
    915 scrollbar_draw(struct scrollbar *s)
    916 {
    917 	off_t y;
    918 
    919 	if (!s->dirty)
    920 		return;
    921 	s->dirty = 0;
    922 	if (s->hidden || !s->size || s->x >= win.width || s->y >= win.height)
    923 		return;
    924 
    925 	cursorsave();
    926 
    927 	/* draw bar (not tick) */
    928 	if (s->focused)
    929 		THEME_SCROLLBAR_FOCUS();
    930 	else
    931 		THEME_SCROLLBAR_NORMAL();
    932 	for (y = 0; y < s->size; y++) {
    933 		if (y >= s->tickpos && y < s->tickpos + s->ticksize)
    934 			continue; /* skip tick */
    935 		cursormove(s->x, s->y + y);
    936 		ttywrite(SCROLLBAR_SYMBOL_BAR);
    937 	}
    938 
    939 	/* draw tick */
    940 	if (s->focused)
    941 		THEME_SCROLLBAR_TICK_FOCUS();
    942 	else
    943 		THEME_SCROLLBAR_TICK_NORMAL();
    944 	for (y = s->tickpos; y < s->size && y < s->tickpos + s->ticksize; y++) {
    945 		cursormove(s->x, s->y + y);
    946 		ttywrite(SCROLLBAR_SYMBOL_TICK);
    947 	}
    948 
    949 	attrmode(ATTR_RESET);
    950 	cursorrestore();
    951 }
    952 
    953 int
    954 readch(void)
    955 {
    956 	unsigned char b;
    957 	fd_set readfds;
    958 	struct timeval tv;
    959 
    960 	if (cmdenv && *cmdenv) {
    961 		b = *(cmdenv++); /* $SFEED_AUTOCMD */
    962 		return (int)b;
    963 	}
    964 
    965 	for (;;) {
    966 		FD_ZERO(&readfds);
    967 		FD_SET(0, &readfds);
    968 		tv.tv_sec = 0;
    969 		tv.tv_usec = 250000; /* 250ms */
    970 		switch (select(1, &readfds, NULL, NULL, &tv)) {
    971 		case -1:
    972 			if (errno != EINTR)
    973 				die("select");
    974 			return -2; /* EINTR: like a signal */
    975 		case 0:
    976 			return -3; /* time-out */
    977 		}
    978 
    979 		switch (read(0, &b, 1)) {
    980 		case -1: die("read");
    981 		case 0: return EOF;
    982 		default: return (int)b;
    983 		}
    984 	}
    985 }
    986 
    987 char *
    988 lineeditor(void)
    989 {
    990 	char *input = NULL;
    991 	size_t cap = 0, nchars = 0;
    992 	int ch;
    993 
    994 	if (usemouse)
    995 		mousemode(0);
    996 	for (;;) {
    997 		if (nchars + 2 >= cap) {
    998 			cap = cap ? cap * 2 : 32;
    999 			input = erealloc(input, cap);
   1000 		}
   1001 
   1002 		ch = readch();
   1003 		if (ch == EOF || ch == '\r' || ch == '\n') {
   1004 			input[nchars] = '\0';
   1005 			break;
   1006 		} else if (ch == '\b' || ch == 0x7f) {
   1007 			if (!nchars)
   1008 				continue;
   1009 			input[--nchars] = '\0';
   1010 			ttywrite("\b \b"); /* back, blank, back */
   1011 		} else if (ch >= ' ') {
   1012 			input[nchars] = ch;
   1013 			input[nchars + 1] = '\0';
   1014 			ttywrite(&input[nchars]);
   1015 			nchars++;
   1016 		} else if (ch < 0) {
   1017 			if (state_sigchld) {
   1018 				state_sigchld = 0;
   1019 				/* wait on child processes so they don't become a zombie */
   1020 				while (waitpid((pid_t)-1, NULL, WNOHANG) > 0)
   1021 					;
   1022 			}
   1023 			if (state_sigint)
   1024 				state_sigint = 0; /* cancel prompt and don't handle this signal */
   1025 			else if (state_sighup || state_sigterm)
   1026 				; /* cancel prompt and handle these signals */
   1027 			else /* no signal, time-out or SIGCHLD or SIGWINCH */
   1028 				continue; /* do not cancel: process signal later */
   1029 
   1030 			free(input);
   1031 			input = NULL;
   1032 			break; /* cancel prompt */
   1033 		}
   1034 	}
   1035 	if (usemouse)
   1036 		mousemode(1);
   1037 	return input;
   1038 }
   1039 
   1040 char *
   1041 uiprompt(int x, int y, char *fmt, ...)
   1042 {
   1043 	va_list ap;
   1044 	char *input, buf[32];
   1045 
   1046 	va_start(ap, fmt);
   1047 	vsnprintf(buf, sizeof(buf), fmt, ap);
   1048 	va_end(ap);
   1049 
   1050 	cursorsave();
   1051 	cursormove(x, y);
   1052 	THEME_INPUT_LABEL();
   1053 	ttywrite(buf);
   1054 	attrmode(ATTR_RESET);
   1055 
   1056 	THEME_INPUT_NORMAL();
   1057 	cleareol();
   1058 	cursormode(1);
   1059 	cursormove(x + colw(buf) + 1, y);
   1060 
   1061 	input = lineeditor();
   1062 	attrmode(ATTR_RESET);
   1063 
   1064 	cursormode(0);
   1065 	cursorrestore();
   1066 
   1067 	return input;
   1068 }
   1069 
   1070 void
   1071 linebar_draw(struct linebar *b)
   1072 {
   1073 	int i;
   1074 
   1075 	if (!b->dirty)
   1076 		return;
   1077 	b->dirty = 0;
   1078 	if (b->hidden || !b->width)
   1079 		return;
   1080 
   1081 	cursorsave();
   1082 	cursormove(b->x, b->y);
   1083 	THEME_LINEBAR();
   1084 	for (i = 0; i < b->width - 1; i++)
   1085 		ttywrite(LINEBAR_SYMBOL_BAR);
   1086 	ttywrite(LINEBAR_SYMBOL_RIGHT);
   1087 	attrmode(ATTR_RESET);
   1088 	cursorrestore();
   1089 }
   1090 
   1091 void
   1092 statusbar_draw(struct statusbar *s)
   1093 {
   1094 	if (!s->dirty)
   1095 		return;
   1096 	s->dirty = 0;
   1097 	if (s->hidden || !s->width || s->x >= win.width || s->y >= win.height)
   1098 		return;
   1099 
   1100 	cursorsave();
   1101 	cursormove(s->x, s->y);
   1102 	THEME_STATUSBAR();
   1103 	/* terminals without xenl (eat newline glitch) mess up scrolling when
   1104 	   using the last cell on the last line on the screen. */
   1105 	printutf8pad(stdout, s->text, s->width - (!eat_newline_glitch), ' ');
   1106 	fflush(stdout);
   1107 	attrmode(ATTR_RESET);
   1108 	cursorrestore();
   1109 }
   1110 
   1111 void
   1112 statusbar_update(struct statusbar *s, const char *text)
   1113 {
   1114 	if (s->text && !strcmp(s->text, text))
   1115 		return;
   1116 
   1117 	free(s->text);
   1118 	s->text = estrdup(text);
   1119 	s->dirty = 1;
   1120 }
   1121 
   1122 /* Line to item, modifies and splits line in-place. */
   1123 int
   1124 linetoitem(char *line, struct item *item)
   1125 {
   1126 	char *fields[FieldLast];
   1127 	time_t parsedtime;
   1128 
   1129 	item->line = line;
   1130 	parseline(line, fields);
   1131 	memcpy(item->fields, fields, sizeof(fields));
   1132 	if (urlfile)
   1133 		item->matchnew = estrdup(fields[fields[FieldLink][0] ? FieldLink : FieldId]);
   1134 	else
   1135 		item->matchnew = NULL;
   1136 
   1137 	parsedtime = 0;
   1138 	if (!strtotime(fields[FieldUnixTimestamp], &parsedtime)) {
   1139 		item->timestamp = parsedtime;
   1140 		item->timeok = 1;
   1141 	} else {
   1142 		item->timestamp = 0;
   1143 		item->timeok = 0;
   1144 	}
   1145 
   1146 	return 0;
   1147 }
   1148 
   1149 void
   1150 feed_items_free(struct items *items)
   1151 {
   1152 	size_t i;
   1153 
   1154 	for (i = 0; i < items->len; i++) {
   1155 		free(items->items[i].line);
   1156 		free(items->items[i].matchnew);
   1157 	}
   1158 	free(items->items);
   1159 	items->items = NULL;
   1160 	items->len = 0;
   1161 	items->cap = 0;
   1162 }
   1163 
   1164 void
   1165 feed_items_get(struct feed *f, FILE *fp, struct items *itemsret)
   1166 {
   1167 	struct item *item, *items = NULL;
   1168 	char *line = NULL;
   1169 	size_t cap, i, linesize = 0, nitems;
   1170 	ssize_t linelen, n;
   1171 	off_t offset;
   1172 
   1173 	cap = nitems = 0;
   1174 	offset = 0;
   1175 	for (i = 0; ; i++) {
   1176 		if (i + 1 >= cap) {
   1177 			cap = cap ? cap * 2 : 16;
   1178 			items = erealloc(items, cap * sizeof(struct item));
   1179 		}
   1180 		if ((n = linelen = getline(&line, &linesize, fp)) > 0) {
   1181 			item = &items[i];
   1182 
   1183 			item->offset = offset;
   1184 			offset += linelen;
   1185 
   1186 			if (line[linelen - 1] == '\n')
   1187 				line[--linelen] = '\0';
   1188 
   1189 			if (lazyload && f->path) {
   1190 				linetoitem(line, item);
   1191 
   1192 				/* data is ignored here, will be lazy-loaded later. */
   1193 				item->line = NULL;
   1194 				memset(item->fields, 0, sizeof(item->fields));
   1195 			} else {
   1196 				linetoitem(estrdup(line), item);
   1197 			}
   1198 
   1199 			nitems++;
   1200 		}
   1201 		if (ferror(fp))
   1202 			die("getline: %s", f->name);
   1203 		if (n <= 0 || feof(fp))
   1204 			break;
   1205 	}
   1206 	itemsret->items = items;
   1207 	itemsret->len = nitems;
   1208 	itemsret->cap = cap;
   1209 	free(line);
   1210 }
   1211 
   1212 void
   1213 updatenewitems(struct feed *f)
   1214 {
   1215 	struct pane *p;
   1216 	struct row *row;
   1217 	struct item *item;
   1218 	size_t i;
   1219 
   1220 	p = &panes[PaneItems];
   1221 	p->dirty = 1;
   1222 	f->totalnew = 0;
   1223 	for (i = 0; i < p->nrows; i++) {
   1224 		row = &(p->rows[i]); /* do not use pane_row_get() */
   1225 		item = row->data;
   1226 		if (urlfile)
   1227 			item->isnew = !urls_hasmatch(&urls, item->matchnew);
   1228 		else
   1229 			item->isnew = (item->timeok && item->timestamp >= comparetime);
   1230 		row->bold = item->isnew;
   1231 		f->totalnew += item->isnew;
   1232 	}
   1233 	f->total = p->nrows;
   1234 }
   1235 
   1236 void
   1237 feed_load(struct feed *f, FILE *fp)
   1238 {
   1239 	/* static, reuse local buffers */
   1240 	static struct items items;
   1241 	struct pane *p;
   1242 	size_t i;
   1243 
   1244 	feed_items_free(&items);
   1245 	feed_items_get(f, fp, &items);
   1246 	p = &panes[PaneItems];
   1247 	p->pos = 0;
   1248 	p->nrows = items.len;
   1249 	free(p->rows);
   1250 	p->rows = ecalloc(sizeof(p->rows[0]), items.len + 1);
   1251 	for (i = 0; i < items.len; i++)
   1252 		p->rows[i].data = &(items.items[i]); /* do not use pane_row_get() */
   1253 
   1254 	updatenewitems(f);
   1255 }
   1256 
   1257 void
   1258 feed_count(struct feed *f, FILE *fp)
   1259 {
   1260 	char *fields[FieldLast];
   1261 	char *line = NULL;
   1262 	size_t linesize = 0;
   1263 	ssize_t linelen;
   1264 	time_t parsedtime;
   1265 
   1266 	f->totalnew = f->total = 0;
   1267 	while ((linelen = getline(&line, &linesize, fp)) > 0) {
   1268 		if (line[linelen - 1] == '\n')
   1269 			line[--linelen] = '\0';
   1270 		parseline(line, fields);
   1271 
   1272 		if (urlfile) {
   1273 			f->totalnew += !urls_hasmatch(&urls, fields[fields[FieldLink][0] ? FieldLink : FieldId]);
   1274 		} else {
   1275 			parsedtime = 0;
   1276 			if (!strtotime(fields[FieldUnixTimestamp], &parsedtime))
   1277 				f->totalnew += (parsedtime >= comparetime);
   1278 		}
   1279 		f->total++;
   1280 	}
   1281 	if (ferror(fp))
   1282 		die("getline: %s", f->name);
   1283 	free(line);
   1284 }
   1285 
   1286 void
   1287 feed_setenv(struct feed *f)
   1288 {
   1289 	if (f && f->path)
   1290 		setenv("SFEED_FEED_PATH", f->path, 1);
   1291 	else
   1292 		unsetenv("SFEED_FEED_PATH");
   1293 }
   1294 
   1295 /* Change feed, have one file open, reopen file if needed. */
   1296 void
   1297 feeds_set(struct feed *f)
   1298 {
   1299 	if (curfeed) {
   1300 		if (curfeed->path && curfeed->fp) {
   1301 			fclose(curfeed->fp);
   1302 			curfeed->fp = NULL;
   1303 		}
   1304 	}
   1305 
   1306 	if (f && f->path) {
   1307 		if (!f->fp && !(f->fp = fopen(f->path, "rb")))
   1308 			die("fopen: %s", f->path);
   1309 	}
   1310 
   1311 	feed_setenv(f);
   1312 
   1313 	curfeed = f;
   1314 }
   1315 
   1316 void
   1317 feeds_load(struct feed *feeds, size_t nfeeds)
   1318 {
   1319 	struct feed *f;
   1320 	size_t i;
   1321 
   1322 	errno = 0;
   1323 	if ((comparetime = time(NULL)) == (time_t)-1)
   1324 		die("time");
   1325 	/* 1 day is old news */
   1326 	comparetime -= 86400;
   1327 
   1328 	for (i = 0; i < nfeeds; i++) {
   1329 		f = &feeds[i];
   1330 
   1331 		if (f->path) {
   1332 			if (f->fp) {
   1333 				if (fseek(f->fp, 0, SEEK_SET))
   1334 					die("fseek: %s", f->path);
   1335 			} else {
   1336 				if (!(f->fp = fopen(f->path, "rb")))
   1337 					die("fopen: %s", f->path);
   1338 			}
   1339 		}
   1340 		if (!f->fp) {
   1341 			/* reading from stdin, just recount new */
   1342 			if (f == curfeed)
   1343 				updatenewitems(f);
   1344 			continue;
   1345 		}
   1346 
   1347 		/* load first items, because of first selection or stdin. */
   1348 		if (f == curfeed) {
   1349 			feed_load(f, f->fp);
   1350 		} else {
   1351 			feed_count(f, f->fp);
   1352 			if (f->path && f->fp) {
   1353 				fclose(f->fp);
   1354 				f->fp = NULL;
   1355 			}
   1356 		}
   1357 	}
   1358 }
   1359 
   1360 /* find row position of the feed if visible, else return -1 */
   1361 off_t
   1362 feeds_row_get(struct pane *p, struct feed *f)
   1363 {
   1364 	struct row *row;
   1365 	struct feed *fr;
   1366 	off_t pos;
   1367 
   1368 	for (pos = 0; pos < p->nrows; pos++) {
   1369 		if (!(row = pane_row_get(p, pos)))
   1370 			continue;
   1371 		fr = row->data;
   1372 		if (!strcmp(fr->name, f->name))
   1373 			return pos;
   1374 	}
   1375 	return -1;
   1376 }
   1377 
   1378 void
   1379 feeds_reloadall(void)
   1380 {
   1381 	struct pane *p;
   1382 	struct feed *f = NULL;
   1383 	struct row *row;
   1384 	off_t pos;
   1385 
   1386 	p = &panes[PaneFeeds];
   1387 	if ((row = pane_row_get(p, p->pos)))
   1388 		f = row->data;
   1389 
   1390 	pos = panes[PaneItems].pos; /* store numeric item position */
   1391 	feeds_set(curfeed); /* close and reopen feed if possible */
   1392 	urls_read(&urls, urlfile);
   1393 	feeds_load(feeds, nfeeds);
   1394 	urls_free(&urls);
   1395 	/* restore numeric item position */
   1396 	pane_setpos(&panes[PaneItems], pos);
   1397 	updatesidebar();
   1398 	updatetitle();
   1399 
   1400 	/* try to find the same feed in the pane */
   1401 	if (f && (pos = feeds_row_get(p, f)) != -1)
   1402 		pane_setpos(p, pos);
   1403 	else
   1404 		pane_setpos(p, 0);
   1405 }
   1406 
   1407 void
   1408 feed_open_selected(struct pane *p)
   1409 {
   1410 	struct feed *f;
   1411 	struct row *row;
   1412 
   1413 	if (!(row = pane_row_get(p, p->pos)))
   1414 		return;
   1415 	f = row->data;
   1416 	feeds_set(f);
   1417 	urls_read(&urls, urlfile);
   1418 	if (f->fp)
   1419 		feed_load(f, f->fp);
   1420 	urls_free(&urls);
   1421 	/* redraw row: counts could be changed */
   1422 	updatesidebar();
   1423 	updatetitle();
   1424 
   1425 	if (layout == LayoutMonocle) {
   1426 		selpane = PaneItems;
   1427 		updategeom();
   1428 	}
   1429 }
   1430 
   1431 void
   1432 feed_plumb_selected_item(struct pane *p, int field)
   1433 {
   1434 	struct row *row;
   1435 	struct item *item;
   1436 	char *cmd[3]; /* will have: { plumbercmd, arg, NULL } */
   1437 
   1438 	if (!(row = pane_row_get(p, p->pos)))
   1439 		return;
   1440 	markread(p, p->pos, p->pos, 1);
   1441 	item = row->data;
   1442 	cmd[0] = plumbercmd;
   1443 	cmd[1] = item->fields[field]; /* set first argument for plumber */
   1444 	cmd[2] = NULL;
   1445 	forkexec(cmd, plumberia);
   1446 }
   1447 
   1448 void
   1449 feed_pipe_selected_item(struct pane *p)
   1450 {
   1451 	struct row *row;
   1452 	struct item *item;
   1453 
   1454 	if (!(row = pane_row_get(p, p->pos)))
   1455 		return;
   1456 	item = row->data;
   1457 	markread(p, p->pos, p->pos, 1);
   1458 	pipeitem(pipercmd, item, -1, piperia);
   1459 }
   1460 
   1461 void
   1462 feed_yank_selected_item(struct pane *p, int field)
   1463 {
   1464 	struct row *row;
   1465 	struct item *item;
   1466 
   1467 	if (!(row = pane_row_get(p, p->pos)))
   1468 		return;
   1469 	item = row->data;
   1470 	pipeitem(yankercmd, item, field, yankeria);
   1471 }
   1472 
   1473 /* calculate optimal (default) size */
   1474 int
   1475 getsidebarsizedefault(void)
   1476 {
   1477 	struct feed *feed;
   1478 	size_t i;
   1479 	int len, size;
   1480 
   1481 	switch (layout) {
   1482 	case LayoutVertical:
   1483 		for (i = 0, size = 0; i < nfeeds; i++) {
   1484 			feed = &feeds[i];
   1485 			len = snprintf(NULL, 0, " (%lu/%lu)",
   1486 			               feed->totalnew, feed->total) +
   1487 				       colw(feed->name);
   1488 			if (len > size)
   1489 				size = len;
   1490 
   1491 			if (onlynew && feed->totalnew == 0)
   1492 				continue;
   1493 		}
   1494 		return MAX(MIN(win.width - 1, size), 0);
   1495 	case LayoutHorizontal:
   1496 		for (i = 0, size = 0; i < nfeeds; i++) {
   1497 			feed = &feeds[i];
   1498 			if (onlynew && feed->totalnew == 0)
   1499 				continue;
   1500 			size++;
   1501 		}
   1502 		return MAX(MIN((win.height - 1) / 2, size), 1);
   1503 	}
   1504 	return 0;
   1505 }
   1506 
   1507 int
   1508 getsidebarsize(void)
   1509 {
   1510 	int size;
   1511 
   1512 	if ((size = fixedsidebarsizes[layout]) < 0)
   1513 		size = getsidebarsizedefault();
   1514 	return size;
   1515 }
   1516 
   1517 void
   1518 adjustsidebarsize(int n)
   1519 {
   1520 	int size;
   1521 
   1522 	if ((size = fixedsidebarsizes[layout]) < 0)
   1523 		size = getsidebarsizedefault();
   1524 	if (n > 0) {
   1525 		if ((layout == LayoutVertical && size + 1 < win.width) ||
   1526 		    (layout == LayoutHorizontal && size + 1 < win.height))
   1527 			size++;
   1528 	} else if (n < 0) {
   1529 		if ((layout == LayoutVertical && size > 0) ||
   1530 		    (layout == LayoutHorizontal && size > 1))
   1531 			size--;
   1532 	}
   1533 
   1534 	if (size != fixedsidebarsizes[layout]) {
   1535 		fixedsidebarsizes[layout] = size;
   1536 		updategeom();
   1537 	}
   1538 }
   1539 
   1540 void
   1541 updatesidebar(void)
   1542 {
   1543 	struct pane *p;
   1544 	struct row *row;
   1545 	struct feed *feed;
   1546 	size_t i, nrows;
   1547 	int oldvalue = 0, newvalue = 0;
   1548 
   1549 	p = &panes[PaneFeeds];
   1550 	if (!p->rows)
   1551 		p->rows = ecalloc(sizeof(p->rows[0]), nfeeds + 1);
   1552 
   1553 	switch (layout) {
   1554 	case LayoutVertical:
   1555 		oldvalue = p->width;
   1556 		newvalue = getsidebarsize();
   1557 		p->width = newvalue;
   1558 		break;
   1559 	case LayoutHorizontal:
   1560 		oldvalue = p->height;
   1561 		newvalue = getsidebarsize();
   1562 		p->height = newvalue;
   1563 		break;
   1564 	}
   1565 
   1566 	nrows = 0;
   1567 	for (i = 0; i < nfeeds; i++) {
   1568 		feed = &feeds[i];
   1569 
   1570 		row = &(p->rows[nrows]);
   1571 		row->bold = (feed->totalnew > 0);
   1572 		row->data = feed;
   1573 
   1574 		if (onlynew && feed->totalnew == 0)
   1575 			continue;
   1576 
   1577 		nrows++;
   1578 	}
   1579 	p->nrows = nrows;
   1580 
   1581 	if (oldvalue != newvalue)
   1582 		updategeom();
   1583 	else
   1584 		p->dirty = 1;
   1585 
   1586 	if (!p->nrows)
   1587 		p->pos = 0;
   1588 	else if (p->pos >= p->nrows)
   1589 		p->pos = p->nrows - 1;
   1590 }
   1591 
   1592 void
   1593 sighandler(int signo)
   1594 {
   1595 	switch (signo) {
   1596 	case SIGCHLD:  state_sigchld = 1;  break;
   1597 	case SIGHUP:   state_sighup = 1;   break;
   1598 	case SIGINT:   state_sigint = 1;   break;
   1599 	case SIGTERM:  state_sigterm = 1;  break;
   1600 	case SIGWINCH: state_sigwinch = 1; break;
   1601 	}
   1602 }
   1603 
   1604 void
   1605 alldirty(void)
   1606 {
   1607 	win.dirty = 1;
   1608 	panes[PaneFeeds].dirty = 1;
   1609 	panes[PaneItems].dirty = 1;
   1610 	scrollbars[PaneFeeds].dirty = 1;
   1611 	scrollbars[PaneItems].dirty = 1;
   1612 	linebar.dirty = 1;
   1613 	statusbar.dirty = 1;
   1614 }
   1615 
   1616 void
   1617 draw(void)
   1618 {
   1619 	struct row *row;
   1620 	struct item *item;
   1621 	size_t i;
   1622 
   1623 	if (win.dirty)
   1624 		win.dirty = 0;
   1625 
   1626 	for (i = 0; i < LEN(panes); i++) {
   1627 		pane_setfocus(&panes[i], i == selpane);
   1628 		pane_draw(&panes[i]);
   1629 
   1630 		/* each pane has a scrollbar */
   1631 		scrollbar_setfocus(&scrollbars[i], i == selpane);
   1632 		scrollbar_update(&scrollbars[i],
   1633 		                 panes[i].pos - (panes[i].pos % panes[i].height),
   1634 		                 panes[i].nrows, panes[i].height);
   1635 		scrollbar_draw(&scrollbars[i]);
   1636 	}
   1637 
   1638 	linebar_draw(&linebar);
   1639 
   1640 	/* if item selection text changed then update the status text */
   1641 	if ((row = pane_row_get(&panes[PaneItems], panes[PaneItems].pos))) {
   1642 		item = row->data;
   1643 		statusbar_update(&statusbar, item->fields[FieldLink]);
   1644 	} else {
   1645 		statusbar_update(&statusbar, "");
   1646 	}
   1647 	statusbar_draw(&statusbar);
   1648 }
   1649 
   1650 void
   1651 mousereport(int button, int release, int keymask, int x, int y)
   1652 {
   1653 	struct pane *p;
   1654 	size_t i;
   1655 	off_t pos;
   1656 	int changedpane, dblclick;
   1657 
   1658 	if (!usemouse || release || button == -1)
   1659 		return;
   1660 
   1661 	for (i = 0; i < LEN(panes); i++) {
   1662 		p = &panes[i];
   1663 		if (p->hidden || !p->width || !p->height)
   1664 			continue;
   1665 
   1666 		/* these button actions are done regardless of the position */
   1667 		switch (button) {
   1668 		case 7: /* side-button: backward */
   1669 			if (selpane == PaneFeeds)
   1670 				return;
   1671 			selpane = PaneFeeds;
   1672 			if (layout == LayoutMonocle)
   1673 				updategeom();
   1674 			return;
   1675 		case 8: /* side-button: forward */
   1676 			if (selpane == PaneItems)
   1677 				return;
   1678 			selpane = PaneItems;
   1679 			if (layout == LayoutMonocle)
   1680 				updategeom();
   1681 			return;
   1682 		}
   1683 
   1684 		/* check if mouse position is in pane or in its scrollbar */
   1685 		if (!(x >= p->x && x < p->x + p->width + (!scrollbars[i].hidden) &&
   1686 		      y >= p->y && y < p->y + p->height))
   1687 			continue;
   1688 
   1689 		changedpane = (selpane != i);
   1690 		selpane = i;
   1691 		/* relative position on screen */
   1692 		pos = y - p->y + p->pos - (p->pos % p->height);
   1693 		dblclick = (pos == p->pos); /* clicking the already selected row */
   1694 
   1695 		switch (button) {
   1696 		case 0: /* left-click */
   1697 			if (!p->nrows || pos >= p->nrows)
   1698 				break;
   1699 			pane_setpos(p, pos);
   1700 			if (i == PaneFeeds)
   1701 				feed_open_selected(&panes[PaneFeeds]);
   1702 			else if (i == PaneItems && dblclick && !changedpane)
   1703 				feed_plumb_selected_item(&panes[PaneItems], FieldLink);
   1704 			break;
   1705 		case 2: /* right-click */
   1706 			if (!p->nrows || pos >= p->nrows)
   1707 				break;
   1708 			pane_setpos(p, pos);
   1709 			if (i == PaneItems)
   1710 				feed_pipe_selected_item(&panes[PaneItems]);
   1711 			break;
   1712 		case 3: /* scroll up */
   1713 		case 4: /* scroll down */
   1714 			pane_scrollpage(p, button == 3 ? -1 : +1);
   1715 			break;
   1716 		}
   1717 		return; /* do not bubble events */
   1718 	}
   1719 }
   1720 
   1721 /* Custom formatter for feed row. */
   1722 char *
   1723 feed_row_format(struct pane *p, struct row *row)
   1724 {
   1725 	/* static, reuse local buffers */
   1726 	static char *bufw, *text;
   1727 	static size_t bufwsize, textsize;
   1728 	struct feed *feed;
   1729 	size_t needsize;
   1730 	char counts[128];
   1731 	int len, w;
   1732 
   1733 	feed = row->data;
   1734 
   1735 	/* align counts to the right and pad the rest with spaces */
   1736 	len = snprintf(counts, sizeof(counts), "(%lu/%lu)",
   1737 	               feed->totalnew, feed->total);
   1738 	if (len > p->width)
   1739 		w = p->width;
   1740 	else
   1741 		w = p->width - len;
   1742 
   1743 	needsize = (w + 1) * 4;
   1744 	if (needsize > bufwsize) {
   1745 		bufw = erealloc(bufw, needsize);
   1746 		bufwsize = needsize;
   1747 	}
   1748 
   1749 	needsize = bufwsize + sizeof(counts) + 1;
   1750 	if (needsize > textsize) {
   1751 		text = erealloc(text, needsize);
   1752 		textsize = needsize;
   1753 	}
   1754 
   1755 	if (utf8pad(bufw, bufwsize, feed->name, w, ' ') != -1)
   1756 		snprintf(text, textsize, "%s%s", bufw, counts);
   1757 	else
   1758 		text[0] = '\0';
   1759 
   1760 	return text;
   1761 }
   1762 
   1763 int
   1764 feed_row_match(struct pane *p, struct row *row, const char *s)
   1765 {
   1766 	struct feed *feed;
   1767 
   1768 	feed = row->data;
   1769 
   1770 	return (strcasestr(feed->name, s) != NULL);
   1771 }
   1772 
   1773 struct row *
   1774 item_row_get(struct pane *p, off_t pos)
   1775 {
   1776 	struct row *itemrow;
   1777 	struct item *item;
   1778 	struct feed *f;
   1779 	char *line = NULL;
   1780 	size_t linesize = 0;
   1781 	ssize_t linelen;
   1782 
   1783 	itemrow = p->rows + pos;
   1784 	item = itemrow->data;
   1785 
   1786 	f = curfeed;
   1787 	if (f && f->path && f->fp && !item->line) {
   1788 		if (fseek(f->fp, item->offset, SEEK_SET))
   1789 			die("fseek: %s", f->path);
   1790 
   1791 		if ((linelen = getline(&line, &linesize, f->fp)) <= 0) {
   1792 			if (ferror(f->fp))
   1793 				die("getline: %s", f->path);
   1794 			return NULL;
   1795 		}
   1796 
   1797 		if (line[linelen - 1] == '\n')
   1798 			line[--linelen] = '\0';
   1799 
   1800 		linetoitem(estrdup(line), item);
   1801 		free(line);
   1802 
   1803 		itemrow->data = item;
   1804 	}
   1805 	return itemrow;
   1806 }
   1807 
   1808 /* Custom formatter for item row. */
   1809 char *
   1810 item_row_format(struct pane *p, struct row *row)
   1811 {
   1812 	/* static, reuse local buffers */
   1813 	static char *text;
   1814 	static size_t textsize;
   1815 	struct item *item;
   1816 	struct tm tm;
   1817 	size_t needsize;
   1818 
   1819 	item = row->data;
   1820 
   1821 	needsize = strlen(item->fields[FieldTitle]) + 21;
   1822 	if (needsize > textsize) {
   1823 		text = erealloc(text, needsize);
   1824 		textsize = needsize;
   1825 	}
   1826 
   1827 	if (item->timeok && localtime_r(&(item->timestamp), &tm)) {
   1828 		snprintf(text, textsize, "%c %04d-%02d-%02d %02d:%02d %s",
   1829 		         item->fields[FieldEnclosure][0] ? '@' : ' ',
   1830 		         tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
   1831 		         tm.tm_hour, tm.tm_min, item->fields[FieldTitle]);
   1832 	} else {
   1833 		snprintf(text, textsize, "%c                  %s",
   1834 		         item->fields[FieldEnclosure][0] ? '@' : ' ',
   1835 		         item->fields[FieldTitle]);
   1836 	}
   1837 
   1838 	return text;
   1839 }
   1840 
   1841 void
   1842 markread(struct pane *p, off_t from, off_t to, int isread)
   1843 {
   1844 	struct row *row;
   1845 	struct item *item;
   1846 	FILE *fp;
   1847 	off_t i;
   1848 	const char *cmd;
   1849 	int isnew = !isread, pid, status = -1, visstart;
   1850 
   1851 	if (!urlfile || !p->nrows)
   1852 		return;
   1853 
   1854 	cmd = isread ? markreadcmd : markunreadcmd;
   1855 
   1856 	switch ((pid = fork())) {
   1857 	case -1:
   1858 		die("fork");
   1859 	case 0:
   1860 		dup2(devnullfd, 1); /* stdout */
   1861 		dup2(devnullfd, 2); /* stderr */
   1862 
   1863 		errno = 0;
   1864 		if (!(fp = popen(cmd, "w")))
   1865 			die("popen: %s", cmd);
   1866 
   1867 		for (i = from; i <= to && i < p->nrows; i++) {
   1868 			/* do not use pane_row_get(): no need for lazyload */
   1869 			row = &(p->rows[i]);
   1870 			item = row->data;
   1871 			if (item->isnew != isnew) {
   1872 				fputs(item->matchnew, fp);
   1873 				putc('\n', fp);
   1874 			}
   1875 		}
   1876 		status = pclose(fp);
   1877 		status = WIFEXITED(status) ? WEXITSTATUS(status) : 127;
   1878 		_exit(status);
   1879 	default:
   1880 		/* waitpid() and block on process status change,
   1881 		   fail if exit statuscode was unavailable or non-zero */
   1882 		if (waitpid(pid, &status, 0) <= 0 || status)
   1883 			break;
   1884 
   1885 		visstart = p->pos - (p->pos % p->height); /* visible start */
   1886 		for (i = from; i <= to && i < p->nrows; i++) {
   1887 			row = &(p->rows[i]);
   1888 			item = row->data;
   1889 			if (item->isnew == isnew)
   1890 				continue;
   1891 
   1892 			row->bold = item->isnew = isnew;
   1893 			curfeed->totalnew += isnew ? 1 : -1;
   1894 
   1895 			/* draw if visible on screen */
   1896 			if (i >= visstart && i < visstart + p->height)
   1897 				pane_row_draw(p, i, i == p->pos);
   1898 		}
   1899 		updatesidebar();
   1900 		updatetitle();
   1901 	}
   1902 }
   1903 
   1904 int
   1905 urls_cmp(const void *v1, const void *v2)
   1906 {
   1907 	return strcmp(*((char **)v1), *((char **)v2));
   1908 }
   1909 
   1910 void
   1911 urls_free(struct urls *urls)
   1912 {
   1913 	while (urls->len > 0) {
   1914 		urls->len--;
   1915 		free(urls->items[urls->len]);
   1916 	}
   1917 	free(urls->items);
   1918 	urls->items = NULL;
   1919 	urls->len = 0;
   1920 	urls->cap = 0;
   1921 }
   1922 
   1923 int
   1924 urls_hasmatch(struct urls *urls, const char *url)
   1925 {
   1926 	return (urls->len &&
   1927 	       bsearch(&url, urls->items, urls->len, sizeof(char *), urls_cmp));
   1928 }
   1929 
   1930 void
   1931 urls_read(struct urls *urls, const char *urlfile)
   1932 {
   1933 	FILE *fp;
   1934 	char *line = NULL;
   1935 	size_t linesiz = 0;
   1936 	ssize_t n;
   1937 
   1938 	urls_free(urls);
   1939 
   1940 	if (!urlfile)
   1941 		return;
   1942 	if (!(fp = fopen(urlfile, "rb")))
   1943 		die("fopen: %s", urlfile);
   1944 
   1945 	while ((n = getline(&line, &linesiz, fp)) > 0) {
   1946 		if (line[n - 1] == '\n')
   1947 			line[--n] = '\0';
   1948 		if (urls->len + 1 >= urls->cap) {
   1949 			urls->cap = urls->cap ? urls->cap * 2 : 16;
   1950 			urls->items = erealloc(urls->items, urls->cap * sizeof(char *));
   1951 		}
   1952 		urls->items[urls->len++] = estrdup(line);
   1953 	}
   1954 	if (ferror(fp))
   1955 		die("getline: %s", urlfile);
   1956 	fclose(fp);
   1957 	free(line);
   1958 
   1959 	if (urls->len > 0)
   1960 		qsort(urls->items, urls->len, sizeof(char *), urls_cmp);
   1961 }
   1962 
   1963 int
   1964 main(int argc, char *argv[])
   1965 {
   1966 	struct pane *p;
   1967 	struct feed *f;
   1968 	struct row *row;
   1969 	char *name, *tmp;
   1970 	char *search = NULL; /* search text */
   1971 	int button, ch, fd, i, keymask, release, x, y;
   1972 	off_t pos;
   1973 
   1974 #ifdef __OpenBSD__
   1975 	if (pledge("stdio rpath tty proc exec", NULL) == -1)
   1976 		die("pledge");
   1977 #endif
   1978 
   1979 	setlocale(LC_CTYPE, "");
   1980 
   1981 	if ((tmp = getenv("SFEED_PLUMBER")))
   1982 		plumbercmd = tmp;
   1983 	if ((tmp = getenv("SFEED_PIPER")))
   1984 		pipercmd = tmp;
   1985 	if ((tmp = getenv("SFEED_YANKER")))
   1986 		yankercmd = tmp;
   1987 	if ((tmp = getenv("SFEED_PLUMBER_INTERACTIVE")))
   1988 		plumberia = !strcmp(tmp, "1");
   1989 	if ((tmp = getenv("SFEED_PIPER_INTERACTIVE")))
   1990 		piperia = !strcmp(tmp, "1");
   1991 	if ((tmp = getenv("SFEED_YANKER_INTERACTIVE")))
   1992 		yankeria = !strcmp(tmp, "1");
   1993 	if ((tmp = getenv("SFEED_MARK_READ")))
   1994 		markreadcmd = tmp;
   1995 	if ((tmp = getenv("SFEED_MARK_UNREAD")))
   1996 		markunreadcmd = tmp;
   1997 	if ((tmp = getenv("SFEED_LAZYLOAD")))
   1998 		lazyload = !strcmp(tmp, "1");
   1999 	urlfile = getenv("SFEED_URL_FILE"); /* can be NULL */
   2000 	cmdenv = getenv("SFEED_AUTOCMD"); /* can be NULL */
   2001 
   2002 	setlayout(argc <= 1 ? LayoutMonocle : LayoutVertical);
   2003 	selpane = layout == LayoutMonocle ? PaneItems : PaneFeeds;
   2004 
   2005 	panes[PaneFeeds].row_format = feed_row_format;
   2006 	panes[PaneFeeds].row_match = feed_row_match;
   2007 	panes[PaneItems].row_format = item_row_format;
   2008 	if (lazyload)
   2009 		panes[PaneItems].row_get = item_row_get;
   2010 
   2011 	feeds = ecalloc(argc, sizeof(struct feed));
   2012 	if (argc == 1) {
   2013 		nfeeds = 1;
   2014 		f = &feeds[0];
   2015 		f->name = "stdin";
   2016 		if (!(f->fp = fdopen(0, "rb")))
   2017 			die("fdopen");
   2018 	} else {
   2019 		for (i = 1; i < argc; i++) {
   2020 			f = &feeds[i - 1];
   2021 			f->path = argv[i];
   2022 			name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i];
   2023 			f->name = name;
   2024 		}
   2025 		nfeeds = argc - 1;
   2026 	}
   2027 	feeds_set(&feeds[0]);
   2028 	urls_read(&urls, urlfile);
   2029 	feeds_load(feeds, nfeeds);
   2030 	urls_free(&urls);
   2031 
   2032 	if (!isatty(0)) {
   2033 		if ((fd = open("/dev/tty", O_RDONLY)) == -1)
   2034 			die("open: /dev/tty");
   2035 		if (dup2(fd, 0) == -1)
   2036 			die("dup2(%d, 0): /dev/tty -> stdin", fd);
   2037 		close(fd);
   2038 	}
   2039 	if (argc == 1)
   2040 		feeds[0].fp = NULL;
   2041 
   2042 	if ((devnullfd = open("/dev/null", O_WRONLY)) == -1)
   2043 		die("open: /dev/null");
   2044 
   2045 	init();
   2046 	updatesidebar();
   2047 	updategeom();
   2048 	updatetitle();
   2049 	draw();
   2050 
   2051 	while (1) {
   2052 		if ((ch = readch()) < 0)
   2053 			goto event;
   2054 		switch (ch) {
   2055 		case '\x1b':
   2056 			if ((ch = readch()) < 0)
   2057 				goto event;
   2058 			if (ch != '[' && ch != 'O')
   2059 				continue; /* unhandled */
   2060 			if ((ch = readch()) < 0)
   2061 				goto event;
   2062 			switch (ch) {
   2063 			case 'M': /* mouse: X10 encoding */
   2064 				if ((ch = readch()) < 0)
   2065 					goto event;
   2066 				button = ch - 32;
   2067 				if ((ch = readch()) < 0)
   2068 					goto event;
   2069 				x = ch - 32;
   2070 				if ((ch = readch()) < 0)
   2071 					goto event;
   2072 				y = ch - 32;
   2073 
   2074 				keymask = button & (4 | 8 | 16); /* shift, meta, ctrl */
   2075 				button &= ~keymask; /* unset key mask */
   2076 
   2077 				/* button numbers (0 - 2) encoded in lowest 2 bits
   2078 				   release does not indicate which button (so set to 0).
   2079 				   Handle extended buttons like scrollwheels
   2080 				   and side-buttons by each range. */
   2081 				release = 0;
   2082 				if (button == 3) {
   2083 					button = -1;
   2084 					release = 1;
   2085 				} else if (button >= 128) {
   2086 					button -= 121;
   2087 				} else if (button >= 64) {
   2088 					button -= 61;
   2089 				}
   2090 				mousereport(button, release, keymask, x - 1, y - 1);
   2091 				break;
   2092 			case '<': /* mouse: SGR encoding */
   2093 				for (button = 0; ; button *= 10, button += ch - '0') {
   2094 					if ((ch = readch()) < 0)
   2095 						goto event;
   2096 					else if (ch == ';')
   2097 						break;
   2098 				}
   2099 				for (x = 0; ; x *= 10, x += ch - '0') {
   2100 					if ((ch = readch()) < 0)
   2101 						goto event;
   2102 					else if (ch == ';')
   2103 						break;
   2104 				}
   2105 				for (y = 0; ; y *= 10, y += ch - '0') {
   2106 					if ((ch = readch()) < 0)
   2107 						goto event;
   2108 					else if (ch == 'm' || ch == 'M')
   2109 						break; /* release or press */
   2110 				}
   2111 				release = ch == 'm';
   2112 				keymask = button & (4 | 8 | 16); /* shift, meta, ctrl */
   2113 				button &= ~keymask; /* unset key mask */
   2114 
   2115 				if (button >= 128)
   2116 					button -= 121;
   2117 				else if (button >= 64)
   2118 					button -= 61;
   2119 
   2120 				mousereport(button, release, keymask, x - 1, y - 1);
   2121 				break;
   2122 			/* DEC/SUN: ESC O char, HP: ESC char or SCO: ESC [ char */
   2123 			case 'A': goto keyup;    /* arrow up */
   2124 			case 'B': goto keydown;  /* arrow down */
   2125 			case 'C': goto keyright; /* arrow right */
   2126 			case 'D': goto keyleft;  /* arrow left */
   2127 			case 'F': goto endpos;   /* end */
   2128 			case 'G': goto nextpage; /* page down */
   2129 			case 'H': goto startpos; /* home */
   2130 			case 'I': goto prevpage; /* page up */
   2131 			default:
   2132 				if (!(ch >= '0' && ch <= '9'))
   2133 					break;
   2134 				for (i = ch - '0'; ;) {
   2135 					if ((ch = readch()) < 0) {
   2136 						goto event;
   2137 					} else if (ch >= '0' && ch <= '9') {
   2138 						i = (i * 10) + (ch - '0');
   2139 						continue;
   2140 					} else if (ch == '~') { /* DEC: ESC [ num ~ */
   2141 						switch (i) {
   2142 						case 1: goto startpos; /* home */
   2143 						case 4: goto endpos;   /* end */
   2144 						case 5: goto prevpage; /* page up */
   2145 						case 6: goto nextpage; /* page down */
   2146 						case 7: goto startpos; /* home: urxvt */
   2147 						case 8: goto endpos;   /* end: urxvt */
   2148 						}
   2149 					} else if (ch == 'z') { /* SUN: ESC [ num z */
   2150 						switch (i) {
   2151 						case 214: goto startpos; /* home */
   2152 						case 216: goto prevpage; /* page up */
   2153 						case 220: goto endpos;   /* end */
   2154 						case 222: goto nextpage; /* page down */
   2155 						}
   2156 					}
   2157 					break;
   2158 				}
   2159 			}
   2160 			break;
   2161 keyup:
   2162 		case 'k':
   2163 			pane_scrolln(&panes[selpane], -1);
   2164 			break;
   2165 keydown:
   2166 		case 'j':
   2167 			pane_scrolln(&panes[selpane], +1);
   2168 			break;
   2169 keyleft:
   2170 		case 'h':
   2171 			if (selpane == PaneFeeds)
   2172 				break;
   2173 			selpane = PaneFeeds;
   2174 			if (layout == LayoutMonocle)
   2175 				updategeom();
   2176 			break;
   2177 keyright:
   2178 		case 'l':
   2179 			if (selpane == PaneItems)
   2180 				break;
   2181 			selpane = PaneItems;
   2182 			if (layout == LayoutMonocle)
   2183 				updategeom();
   2184 			break;
   2185 		case 'K':
   2186 			p = &panes[selpane];
   2187 			if (!p->nrows)
   2188 				break;
   2189 			for (pos = p->pos - 1; pos >= 0; pos--) {
   2190 				if ((row = pane_row_get(p, pos)) && row->bold) {
   2191 					pane_setpos(p, pos);
   2192 					break;
   2193 				}
   2194 			}
   2195 			break;
   2196 		case 'J':
   2197 			p = &panes[selpane];
   2198 			if (!p->nrows)
   2199 				break;
   2200 			for (pos = p->pos + 1; pos < p->nrows; pos++) {
   2201 				if ((row = pane_row_get(p, pos)) && row->bold) {
   2202 					pane_setpos(p, pos);
   2203 					break;
   2204 				}
   2205 			}
   2206 			break;
   2207 		case '\t':
   2208 			selpane = selpane == PaneFeeds ? PaneItems : PaneFeeds;
   2209 			if (layout == LayoutMonocle)
   2210 				updategeom();
   2211 			break;
   2212 startpos:
   2213 		case 'g':
   2214 			pane_setpos(&panes[selpane], 0);
   2215 			break;
   2216 endpos:
   2217 		case 'G':
   2218 			p = &panes[selpane];
   2219 			if (p->nrows)
   2220 				pane_setpos(p, p->nrows - 1);
   2221 			break;
   2222 prevpage:
   2223 		case 2: /* ^B */
   2224 			pane_scrollpage(&panes[selpane], -1);
   2225 			break;
   2226 nextpage:
   2227 		case ' ':
   2228 		case 6: /* ^F */
   2229 			pane_scrollpage(&panes[selpane], +1);
   2230 			break;
   2231 		case '[':
   2232 		case ']':
   2233 			pane_scrolln(&panes[PaneFeeds], ch == '[' ? -1 : +1);
   2234 			feed_open_selected(&panes[PaneFeeds]);
   2235 			break;
   2236 		case '/': /* new search (forward) */
   2237 		case '?': /* new search (backward) */
   2238 		case 'n': /* search again (forward) */
   2239 		case 'N': /* search again (backward) */
   2240 			p = &panes[selpane];
   2241 
   2242 			/* prompt for new input */
   2243 			if (ch == '?' || ch == '/') {
   2244 				tmp = ch == '?' ? "backward" : "forward";
   2245 				free(search);
   2246 				search = uiprompt(statusbar.x, statusbar.y,
   2247 				                  "Search (%s):", tmp);
   2248 				statusbar.dirty = 1;
   2249 			}
   2250 			if (!search || !p->nrows)
   2251 				break;
   2252 
   2253 			if (ch == '/' || ch == 'n') {
   2254 				/* forward */
   2255 				for (pos = p->pos + 1; pos < p->nrows; pos++) {
   2256 					if (pane_row_match(p, pane_row_get(p, pos), search)) {
   2257 						pane_setpos(p, pos);
   2258 						break;
   2259 					}
   2260 				}
   2261 			} else {
   2262 				/* backward */
   2263 				for (pos = p->pos - 1; pos >= 0; pos--) {
   2264 					if (pane_row_match(p, pane_row_get(p, pos), search)) {
   2265 						pane_setpos(p, pos);
   2266 						break;
   2267 					}
   2268 				}
   2269 			}
   2270 			break;
   2271 		case 12: /* ^L, redraw */
   2272 			alldirty();
   2273 			break;
   2274 		case 'R': /* reload all files */
   2275 			feeds_reloadall();
   2276 			break;
   2277 		case 'a': /* attachment */
   2278 		case 'e': /* enclosure */
   2279 		case '@':
   2280 			if (selpane == PaneItems)
   2281 				feed_plumb_selected_item(&panes[selpane], FieldEnclosure);
   2282 			break;
   2283 		case 'm': /* toggle mouse mode */
   2284 			usemouse = !usemouse;
   2285 			mousemode(usemouse);
   2286 			break;
   2287 		case '<': /* decrease fixed sidebar width */
   2288 		case '>': /* increase fixed sidebar width */
   2289 			adjustsidebarsize(ch == '<' ? -1 : +1);
   2290 			break;
   2291 		case '=': /* reset fixed sidebar to automatic size */
   2292 			fixedsidebarsizes[layout] = -1;
   2293 			updategeom();
   2294 			break;
   2295 		case 't': /* toggle showing only new in sidebar */
   2296 			p = &panes[PaneFeeds];
   2297 			if ((row = pane_row_get(p, p->pos)))
   2298 				f = row->data;
   2299 			else
   2300 				f = NULL;
   2301 
   2302 			onlynew = !onlynew;
   2303 			updatesidebar();
   2304 
   2305 			/* try to find the same feed in the pane */
   2306 			if (f && f->totalnew &&
   2307 			    (pos = feeds_row_get(p, f)) != -1)
   2308 				pane_setpos(p, pos);
   2309 			else
   2310 				pane_setpos(p, 0);
   2311 			break;
   2312 		case 'o': /* feeds: load, items: plumb URL */
   2313 		case '\n':
   2314 			if (selpane == PaneFeeds && panes[selpane].nrows)
   2315 				feed_open_selected(&panes[selpane]);
   2316 			else if (selpane == PaneItems && panes[selpane].nrows)
   2317 				feed_plumb_selected_item(&panes[selpane], FieldLink);
   2318 			break;
   2319 		case 'c': /* items: pipe TSV line to program */
   2320 		case 'p':
   2321 		case '|':
   2322 			if (selpane == PaneItems)
   2323 				feed_pipe_selected_item(&panes[selpane]);
   2324 			break;
   2325 		case 'y': /* yank: pipe TSV field to yank URL to clipboard */
   2326 		case 'E': /* yank: pipe TSV field to yank enclosure to clipboard */
   2327 			if (selpane == PaneItems)
   2328 				feed_yank_selected_item(&panes[selpane],
   2329 				                        ch == 'y' ? FieldLink : FieldEnclosure);
   2330 			break;
   2331 		case 'f': /* mark all read */
   2332 		case 'F': /* mark all unread */
   2333 			if (panes[PaneItems].nrows) {
   2334 				p = &panes[PaneItems];
   2335 				markread(p, 0, p->nrows - 1, ch == 'f');
   2336 			}
   2337 			break;
   2338 		case 'r': /* mark item as read */
   2339 		case 'u': /* mark item as unread */
   2340 			if (selpane == PaneItems && panes[selpane].nrows) {
   2341 				p = &panes[selpane];
   2342 				markread(p, p->pos, p->pos, ch == 'r');
   2343 			}
   2344 			break;
   2345 		case 's': /* toggle layout between monocle or non-monocle */
   2346 			setlayout(layout == LayoutMonocle ? prevlayout : LayoutMonocle);
   2347 			updategeom();
   2348 			break;
   2349 		case '1': /* vertical layout */
   2350 		case '2': /* horizontal layout */
   2351 		case '3': /* monocle layout */
   2352 			setlayout(ch - '1');
   2353 			updategeom();
   2354 			break;
   2355 		case 4: /* EOT */
   2356 		case 'q': goto end;
   2357 		}
   2358 event:
   2359 		if (ch == EOF)
   2360 			goto end;
   2361 		else if (ch == -3 && !state_sigchld && !state_sighup &&
   2362 		         !state_sigint && !state_sigterm && !state_sigwinch)
   2363 			continue; /* just a time-out, nothing to do */
   2364 
   2365 		/* handle signals in a particular order */
   2366 		if (state_sigchld) {
   2367 			state_sigchld = 0;
   2368 			/* wait on child processes so they don't become a zombie,
   2369 			   do not block the parent process if there is no status,
   2370 			   ignore errors */
   2371 			while (waitpid((pid_t)-1, NULL, WNOHANG) > 0)
   2372 				;
   2373 		}
   2374 		if (state_sigterm) {
   2375 			cleanup();
   2376 			_exit(128 + SIGTERM);
   2377 		}
   2378 		if (state_sigint) {
   2379 			cleanup();
   2380 			_exit(128 + SIGINT);
   2381 		}
   2382 		if (state_sighup) {
   2383 			state_sighup = 0;
   2384 			feeds_reloadall();
   2385 		}
   2386 		if (state_sigwinch) {
   2387 			state_sigwinch = 0;
   2388 			resizewin();
   2389 			updategeom();
   2390 		}
   2391 
   2392 		draw();
   2393 	}
   2394 end:
   2395 	cleanup();
   2396 
   2397 	return 0;
   2398 }