sfeed_curses.c (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 }