sfeed_curses.c - sfeed_curses - sfeed curses UI (now part of sfeed, development… | |
git clone git://git.codemadness.org/sfeed_curses | |
Log | |
Files | |
Refs | |
README | |
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 horizont… | |
37 #define LINEBAR_SYMBOL_RIGHT "\xe2\x94\xa4" /* symbol: "light vertical… | |
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… | |
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() callb… | |
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 … | |
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 scrollb… | |
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: $SFEE… | |
189 static char *markunreadcmd = "sfeed_markread unread"; /* env variable: $… | |
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… | |
307 terminators and assign these fields as pointers. If there are less fi… | |
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 … | |
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(PA… | |
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… | |
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 … | |
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 … | |
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 + in… | |
473 fputs("\xe2\x80\xa6", fp); /* ellipsis */ | |
474 col++; | |
475 break; | |
476 } else if (rl < 0) { | |
477 fputs("\xef\xbf\xbd", fp); /* replacemen… | |
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, tot… | |
516 } | |
517 | |
518 void | |
519 appmode(int on) | |
520 { | |
521 ttywrite(tparmnull(on ? enter_ca_mode : exit_ca_mode, 0, 0, 0, 0… | |
522 } | |
523 | |
524 void | |
525 mousemode(int on) | |
526 { | |
527 ttywrite(on ? "\x1b[?1000h" : "\x1b[?1000l"); /* xterm X10 mouse… | |
528 ttywrite(on ? "\x1b[?1006h" : "\x1b[?1006l"); /* extended SGR mo… | |
529 } | |
530 | |
531 void | |
532 cursormode(int on) | |
533 { | |
534 ttywrite(tparmnull(on ? cursor_normal : cursor_invisible, 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, … | |
549 } | |
550 | |
551 void | |
552 cursorrestore(void) | |
553 { | |
554 /* if the cursor cannot be hidden then move to a consistent posi… | |
555 if (cursor_invisible) | |
556 ttywrite(tparmnull(restore_cursor, 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… | |
567 break; | |
568 case ATTR_BOLD_ON: | |
569 ttywrite(tparmnull(enter_bold_mode, 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, … | |
573 break; | |
574 case ATTR_REVERSE_ON: | |
575 ttywrite(tparmnull(enter_reverse_mode, 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… | |
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 !=… | |
928 panes[PaneItems].hidden = layout == LayoutMonocle && (selpane !=… | |
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].… | |
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].… | |
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 pagehe… | |
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.he… | |
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… | |
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 qui… | |
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.h… | |
1203 return; | |
1204 | |
1205 cursorsave(); | |
1206 cursormove(s->x, s->y); | |
1207 THEME_STATUSBAR(); | |
1208 /* terminals without xenl (eat newline glitch) mess up scrolling… | |
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] ? F… | |
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-lo… | |
1298 item->line = NULL; | |
1299 memset(item->fields, 0, sizeof(item->fie… | |
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 >… | |
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_… | |
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[FieldLin… | |
1380 } else { | |
1381 parsedtime = 0; | |
1382 if (!strtotime(fields[FieldUnixTimestamp], &pars… | |
1383 f->totalnew += (parsedtime >= comparetim… | |
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 }, p… | |
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]… | |
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 posit… | |
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 scrollba… | |
1788 if (!(x >= p->x && x < p->x + p->width + (!scrollbars[i]… | |
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 twi… | |
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 && !changedp… | |
1806 feed_plumb_selected_item(&panes[PaneItem… | |
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 sta… | |
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) == N… | |
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 +… | |
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); /* shif… | |
2176 button &= ~keymask; /* unset key mask */ | |
2177 | |
2178 /* button numbers (0 - 2) encoded in low… | |
2179 release does not indicate which butto… | |
2180 Handle extended buttons like scrollwh… | |
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 … | |
2192 break; | |
2193 case '<': /* mouse: SGR encoding */ | |
2194 for (button = 0; ; button *= 10, button … | |
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 pre… | |
2211 } | |
2212 release = ch == 'm'; | |
2213 keymask = button & (4 | 8 | 16); /* shif… | |
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 … | |
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 : Pan… | |
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 :… | |
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… | |
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; p… | |
2328 if (pane_row_match(p, pane_row_g… | |
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_g… | |
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]… | |
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].… | |
2390 feed_plumb_selected_item(&panes[selpane]… | |
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 clipboa… | |
2399 case 'E': /* yank: pipe TSV field to yank enclosure to c… | |
2400 if (selpane == PaneItems) | |
2401 feed_yank_selected_item(&panes[selpane], | |
2402 ch == 'y' ? Fiel… | |
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-monocl… | |
2419 setlayout(layout == LayoutMonocle ? prevlayout :… | |
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 } |