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