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