sfeed_curses

[fork] sfeed (atom feed) reader
git clone https://hhvn.uk/sfeed_curses
git clone git://hhvn.uk/sfeed_curses
Log | Files | Refs | README | LICENSE

sfeed_curses.c (44328B)


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