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