sfeed

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

sfeed_curses.c (raw) (52242B)


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