sfeed

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

sfeed_curses.c (raw) (52868B)


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