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