sfeed_curses.c (50157B) - raw


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