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