sfeed

Simple RSS and Atom feed parser
git clone https://git.sinitax.com/codemadness/sfeed
Log | Files | Refs | README | LICENSE | Upstream | sfeed.txt

sfeed_curses.c (52868B)


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