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