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