fa2ea1074272fab58ca71df64f76342bb9045b7b
[smdp.git] / src / viewer.c
1 /*
2  * Functions necessary to display a deck of slides in different color modes
3  * using ncurses. Only white, red, and blue are supported, as they can be
4  * faded in 256 color mode.
5  * Copyright (C) 2014 Michael Goehler
6  *
7  * This file is part of mdp.
8  *
9  * This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program. If not, see <http://www.gnu.org/licenses/>.
21  *
22  */
23
24 #include <ctype.h>  // isalnum
25 #include <locale.h> // setlocale
26 #include <string.h> // strchr
27 #include <unistd.h> // usleep
28
29 #include "viewer.h"
30
31 // color ramp for fading from black to color
32 static short white_ramp[24] = { 16, 232, 233, 234, 235, 236,
33                                237, 238, 239, 240, 241, 242,
34                                244, 245, 246, 247, 248, 249,
35                                250, 251, 252, 253, 254, 255 };
36
37 static short blue_ramp[24]  = { 16,  17,  17,  18,  18,  19,
38                                 19,  20,  20,  21,  27,  33,
39                                 32,  39,  38,  45,  44,  44,
40                                 81,  81,  51,  51, 123, 123 };
41
42 static short red_ramp[24]   = { 16,  52,  52,  53,  53,  89,
43                                 89,  90,  90, 126, 127, 127,
44                                163, 163, 164, 164, 200, 200,
45                                201, 201, 207, 207, 213, 213 };
46
47 // color ramp for fading from white to color
48 static short white_ramp_invert[24] = { 15, 255, 254, 254, 252, 251,
49                                       250, 249, 248, 247, 246, 245,
50                                       243, 242, 241, 240, 239, 238,
51                                       237, 236, 235, 234, 233, 232};
52
53 static short blue_ramp_invert[24]  = { 15, 231, 231, 195, 195, 159,
54                                       159, 123, 123,  87,  51,  44,
55                                        45,  38,  39,  32,  33,  33,
56                                        26,  26,  27,  27,  21,  21};
57
58 static short red_ramp_invert[24]   = { 15, 231, 231, 224, 224, 225,
59                                       225, 218, 218, 219, 212, 213,
60                                       206, 207, 201, 200, 199, 199,
61                                       198, 198, 197, 197, 196, 196};
62
63 int ncurses_display(deck_t *deck, int notrans, int nofade, int invert) {
64
65     int c = 0;          // char
66     int i = 0;          // iterate
67     int l = 0;          // line number
68     int lc = 0;         // line count
69     int sc = 1;         // slide count
70     int colors = 0;     // amount of colors supported
71     int fade = 0;       // disable color fading by default
72     int trans = -1;     // enable transparency if term supports it
73     int max_lines = 0;  // max lines per slide
74     int max_cols = 0;   // max columns per line
75     int offset;         // text offset
76
77     // header line 1 is displayed at the top
78     int bar_top = (deck->headers > 0) ? 1 : 0;
79     // header line 2 is displayed at the bottom
80     // anyway we display the slide number at the bottom
81     int bar_bottom = 1;
82
83     slide_t *slide = deck->slide;
84     line_t *line;
85
86     // set locale to display UTF-8 correctly in ncurses
87     setlocale(LC_CTYPE, "");
88
89     // init ncurses
90     initscr();
91
92     while(slide) {
93         lc = 0;
94         line = slide->line;
95
96         while(line) {
97
98             if (line && line->text && line->text->text)
99                 lc += url_count_inline(line->text->text);
100
101             if(line->length > COLS) {
102                 i = line->length;
103                 offset = 0;
104                 while(i > COLS) {
105
106                     i = prev_blank(line->text, offset + COLS) - offset;
107
108                     // single word is > COLS
109                     if(!i) {
110                         // calculate min_width
111                         i = next_blank(line->text, offset + COLS) - offset;
112
113                         // disable ncurses
114                         endwin();
115
116                         // print error
117                         fprintf(stderr, "Error: Terminal width (%i columns) too small. Need at least %i columns.\n", COLS, i);
118                         fprintf(stderr, "You may need to shorten some lines by inserting line breaks.\n");
119
120                         return 1;
121                     }
122
123                     // set max_cols
124                     max_cols = MAX(i, max_cols);
125
126                     // iterate to next line
127                     offset = prev_blank(line->text, offset + COLS);
128                     i = line->length - offset;
129                     lc++;
130                 }
131                 // set max_cols one last time
132                 max_cols = MAX(i, max_cols);
133             } else {
134                 // set max_cols
135                 max_cols = MAX(line->length, max_cols);
136             }
137             lc++;
138             line = line->next;
139         }
140
141         max_lines = MAX(lc, max_lines);
142
143         slide = slide->next;
144     }
145
146     // not enough lines
147     if(max_lines + bar_top + bar_bottom > LINES) {
148
149         // disable ncurses
150         endwin();
151
152         // print error
153         fprintf(stderr, "Error: Terminal height (%i lines) too small. Need at least %i lines.\n", LINES, max_lines + bar_top + bar_bottom);
154         fprintf(stderr, "You may need to add additional horizontal rules ('***') to split your file in shorter slides.\n");
155
156         return 1;
157     }
158
159     // disable cursor
160     curs_set(0);
161
162     // disable output of keyboard typing
163     noecho();
164
165     // make getch() process one char at a time
166     cbreak();
167
168     // enable arrow keys
169     keypad(stdscr,TRUE);
170
171     // set colors
172     if(has_colors() == TRUE) {
173         start_color();
174         use_default_colors();
175
176         // 256 color mode
177         if(COLORS == 256) {
178
179             if(notrans) {
180                 if(invert) {
181                     trans = 15; // white in 256 color mode
182                 } else {
183                     trans = 16; // black in 256 color mode
184                 }
185             }
186
187             if(invert) {
188                 init_pair(CP_WHITE, 232, trans);
189                 init_pair(CP_BLUE, 21, trans);
190                 init_pair(CP_RED, 196, trans);
191                 init_pair(CP_BLACK, 15, 232);
192             } else {
193                 init_pair(CP_WHITE, 255, trans);
194                 init_pair(CP_BLUE, 123, trans);
195                 init_pair(CP_RED, 213, trans);
196                 init_pair(CP_BLACK, 16, 255);
197             }
198             init_pair(CP_YELLOW, 208, trans);
199
200             // enable color fading
201             if(!nofade)
202                 fade = true;
203
204         // 8 color mode
205         } else {
206
207             if(notrans) {
208                 if(invert) {
209                     trans = 7; // white in 8 color mode
210                 } else {
211                     trans = 0; // black in 8 color mode
212                 }
213             }
214
215             if(invert) {
216                 init_pair(CP_WHITE, 0, trans);
217                 init_pair(CP_BLACK, 7, 0);
218             } else {
219                 init_pair(CP_WHITE, 7, trans);
220                 init_pair(CP_BLACK, 0, 7);
221             }
222             init_pair(CP_BLUE, 4, trans);
223             init_pair(CP_RED, 1, trans);
224             init_pair(CP_YELLOW, 3, trans);
225         }
226
227         colors = 1;
228     }
229
230     // set background color of main window
231     if(colors)
232         wbkgd(stdscr, COLOR_PAIR(CP_YELLOW));
233
234     // setup main window
235     WINDOW *content = newwin(LINES - bar_top - bar_bottom, COLS, 0 + bar_top, 0);
236     if(colors)
237         wbkgd(content, COLOR_PAIR(CP_WHITE));
238
239     slide = deck->slide;
240     while(slide) {
241
242         url_init();
243
244         // clear windows
245         werase(content);
246         werase(stdscr);
247
248         // always resize window in case terminal geometry has changed
249         wresize(content, LINES - bar_top - bar_bottom, COLS);
250
251         // setup header
252         if(bar_top) {
253             line = deck->header;
254             offset = next_blank(line->text, 0) + 1;
255             // add text to header
256             mvwprintw(stdscr,
257                       0, (COLS - line->length + offset) / 2,
258                       "%s", &line->text->text[offset]);
259         }
260
261         // setup footer
262         if(deck->headers > 1) {
263             line = deck->header->next;
264             offset = next_blank(line->text, 0) + 1;
265             // add text to left footer
266             mvwprintw(stdscr,
267                       LINES - 1, 3,
268                       "%s", &line->text->text[offset]);
269         }
270         // add slide number to right footer
271         mvwprintw(stdscr,
272                   LINES - 1, COLS - int_length(deck->slides) - int_length(sc) - 6,
273                   "%d / %d", sc, deck->slides);
274
275         // make header + fooder visible
276         wrefresh(content);
277         wrefresh(stdscr);
278
279         line = slide->line;
280         l = 0;
281
282         // print lines
283         while(line) {
284             add_line(content, l, (COLS - max_cols) / 2, line, max_cols, colors);
285             l += (line->length / COLS) + 1;
286             line = line->next;
287         }
288
289         int i, ymax;
290         getmaxyx( content, ymax, i );
291         for (i = 0; i < url_get_amount(); i++) {
292             mvwprintw(content, ymax - url_get_amount() - 1 + i, 3,
293                       "[%d] %s", i, url_get_target(i));
294         }
295
296         // make content visible
297         wrefresh(content);
298
299         // fade in
300         if(fade)
301             fade_in(content, trans, colors, invert);
302
303         // re-enable fading after any undefined key press
304         if(COLORS == 256 && !nofade)
305             fade = true;
306
307         // wait for user input
308         c = getch();
309
310         // evaluate user input
311         i = 0;
312         switch(c) {
313
314             // show previous slide
315             case KEY_UP:
316             case KEY_LEFT:
317             case KEY_PPAGE:
318             case 8:   // BACKSPACE (ascii)
319             case 127: // BACKSPACE (xterm)
320             case 263: // BACKSPACE (getty)
321             case 'h':
322             case 'k':
323                 if(slide->prev) {
324                     slide = slide->prev;
325                     sc--;
326                 } else {
327                     fade = false;
328                 }
329                 break;
330
331             // show next slide
332             case KEY_DOWN:
333             case KEY_RIGHT:
334             case KEY_NPAGE:
335             case '\n': // ENTER
336             case ' ':  // SPACE
337             case 'j':
338             case 'l':
339                 if(slide->next) {
340                     slide = slide->next;
341                     sc++;
342                 } else {
343                     fade = false;
344                 }
345                 break;
346
347             // show slide n
348             case '9': i++;
349             case '8': i++;
350             case '7': i++;
351             case '6': i++;
352             case '5': i++;
353             case '4': i++;
354             case '3': i++;
355             case '2': i++;
356             case '1': i++;
357                 if(i <= deck->slides) {
358                     while(sc != i) {
359                         // search forward
360                         if(sc < i) {
361                             if(slide->next) {
362                                 slide = slide->next;
363                                 sc++;
364                             }
365                         // search backward
366                         } else {
367                             if(slide->prev) {
368                                 slide = slide->prev;
369                                 sc--;
370                             }
371                         }
372                     }
373                 } else {
374                     // disable fading if slide n doesn't exist
375                     fade = false;
376                 }
377                 break;
378
379             // show first slide
380             case KEY_HOME:
381                 slide = deck->slide;
382                 sc = 1;
383                 break;
384
385             // show last slide
386             case KEY_END:
387                 for(i = sc; i <= deck->slides; i++) {
388                     if(slide->next) {
389                             slide = slide->next;
390                             sc++;
391                     }
392                 }
393                 break;
394
395             // quit
396             case 'q':
397                 // do not fade out on exit
398                 fade = false;
399                 slide = NULL;
400                 break;
401
402             default:
403                 // disable fading on undefined key press
404                 fade = false;
405                 break;
406         }
407
408         // fade out
409         if(fade)
410             fade_out(content, trans, colors, invert);
411
412         url_purge();
413     }
414
415     endwin();
416
417     return 0;
418 }
419
420 void add_line(WINDOW *window, int y, int x, line_t *line, int max_cols, int colors) {
421
422     if(!line->text->text) {
423         return;
424     }
425
426     int i; // increment
427     int offset = 0; // text offset
428
429     // move the cursor in position
430     wmove(window, y, x);
431
432     // IS_UNORDERED_LIST_3
433     if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_3)) {
434         offset = next_nonblank(line->text, 0);
435         char prompt[13];
436         strcpy(&prompt[0], CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)? " |  " : "    ");
437         strcpy(&prompt[4], CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)? " |  " : "    ");
438
439         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_EXT)) {
440             strcpy(&prompt[8], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_3)? " |  " : "    ");
441         } else {
442             strcpy(&prompt[8], " +- ");
443             offset += 2;
444         }
445
446         wprintw(window,
447                 "%s", prompt);
448
449         if(!CHECK_BIT(line->bits, IS_CODE))
450             inline_display(window, &line->text->text[offset], colors);
451
452     // IS_UNORDERED_LIST_2
453     } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)) {
454         offset = next_nonblank(line->text, 0);
455         char prompt[9];
456         strcpy(&prompt[0], CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)? " |  " : "    ");
457
458         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_EXT)) {
459             strcpy(&prompt[4], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_2)? " |  " : "    ");
460         } else {
461             strcpy(&prompt[4], " +- ");
462             offset += 2;
463         }
464
465         wprintw(window,
466                 "%s", prompt);
467
468         if(!CHECK_BIT(line->bits, IS_CODE))
469             inline_display(window, &line->text->text[offset], colors);
470
471     // IS_UNORDERED_LIST_1
472     } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)) {
473         offset = next_nonblank(line->text, 0);
474         char prompt[5];
475
476         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_EXT)) {
477             strcpy(&prompt[0], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_1)? " |  " : "    ");
478         } else {
479             strcpy(&prompt[0], " +- ");
480             offset += 2;
481         }
482
483         wprintw(window,
484                 "%s", prompt);
485
486         if(!CHECK_BIT(line->bits, IS_CODE))
487             inline_display(window, &line->text->text[offset], colors);
488     }
489
490     // IS_CODE
491     if(CHECK_BIT(line->bits, IS_CODE)) {
492
493         // set static offset for code
494         offset = CODE_INDENT;
495
496         // reverse color for code blocks
497         if(colors)
498             wattron(window, COLOR_PAIR(CP_BLACK));
499
500         // print whole lines
501         wprintw(window,
502                 "%s", &line->text->text[offset]);
503     }
504
505     if(!CHECK_BIT(line->bits, IS_UNORDERED_LIST_1) &&
506        !CHECK_BIT(line->bits, IS_UNORDERED_LIST_2) &&
507        !CHECK_BIT(line->bits, IS_UNORDERED_LIST_3) &&
508        !CHECK_BIT(line->bits, IS_CODE)) {
509
510         // IS_QUOTE
511         if(CHECK_BIT(line->bits, IS_QUOTE)) {
512             while(line->text->text[offset] == '>') {
513                 // print a reverse color block
514                 if(colors) {
515                     wattron(window, COLOR_PAIR(CP_BLACK));
516                     wprintw(window, "%s", " ");
517                     wattron(window, COLOR_PAIR(CP_WHITE));
518                     wprintw(window, "%s", " ");
519                 } else {
520                     wprintw(window, "%s", ">");
521                 }
522
523                 // find next quote or break
524                 offset++;
525                 if(line->text->text[offset] == ' ')
526                     offset = next_word(line->text, offset);
527             }
528
529             inline_display(window, &line->text->text[offset], colors);
530         } else {
531
532             // IS_CENTER
533             if(CHECK_BIT(line->bits, IS_CENTER)) {
534                 if(line->length < max_cols) {
535                     wmove(window, y, x + ((max_cols - line->length) / 2));
536                 }
537             }
538
539             // IS_H1 || IS_H2
540             if(CHECK_BIT(line->bits, IS_H1) || CHECK_BIT(line->bits, IS_H2)) {
541
542                 // set headline color
543                 if(colors)
544                     wattron(window, COLOR_PAIR(CP_BLUE));
545
546                 // enable underline for H1
547                 if(CHECK_BIT(line->bits, IS_H1))
548                     wattron(window, A_UNDERLINE);
549
550                 // skip hashes
551                 while(line->text->text[offset] == '#')
552                     offset = next_word(line->text, offset);
553
554                 // print whole lines
555                 wprintw(window,
556                         "%s", &line->text->text[offset]);
557
558                 wattroff(window, A_UNDERLINE);
559
560             // no line-wide markdown
561             } else {
562
563                 inline_display(window, &line->text->text[offset], colors);
564             }
565         }
566     }
567
568     // fill rest off line with spaces
569     for(i = getcurx(window) - x; i < max_cols; i++)
570         wprintw(window, "%s", " ");
571
572     // reset to default color
573     if(colors)
574         wattron(window, COLOR_PAIR(CP_WHITE));
575     wattroff(window, A_UNDERLINE);
576 }
577
578 void inline_display(WINDOW *window, const char *c, const int colors) {
579     const static char *special = "\\*_`!["; // list of interpreted chars
580     const char *i = c; // iterator
581     const char *start_link_name, *start_url;
582     int length_link_name, url_num;
583     cstack_t *stack = cstack_init();
584
585
586     // for each char in line
587     for(; *i; i++) {
588
589         // if char is in special char list
590         if(strchr(special, *i)) {
591
592             // closing special char (or second backslash)
593             // only if not followed by :alnum:
594             if((stack->top)(stack, *i) &&
595                (!isalnum((int)i[1]) || *(i + 1) == '\0' || *i == '\\')) {
596
597                 switch(*i) {
598                     // print escaped backslash
599                     case '\\':
600                         wprintw(window, "%c", *i);
601                         break;
602                     // disable highlight
603                     case '*':
604                         if(colors)
605                             wattron(window, COLOR_PAIR(CP_WHITE));
606                         break;
607                     // disable underline
608                     case '_':
609                         wattroff(window, A_UNDERLINE);
610                         break;
611                     // disable inline code
612                     case '`':
613                         if(colors)
614                             wattron(window, COLOR_PAIR(CP_WHITE));
615                         break;
616                 }
617
618                 // remove top special char from stack
619                 (stack->pop)(stack);
620
621             // treat special as regular char
622             } else if((stack->top)(stack, '\\')) {
623                 wprintw(window, "%c", *i);
624
625                 // remove backslash from stack
626                 (stack->pop)(stack);
627
628             // opening special char
629             } else {
630
631                 // emphasis or code span can start after new-line or space only
632                 // and of cause after another emphasis markup
633                 //TODO this condition looks ugly
634                 if(i == c ||
635                    *(i - 1) == ' ' ||
636                    ((*(i - 1) == '_' || *(i - 1) == '*') && ((i - 1) == c || *(i - 2) == ' ')) ||
637                    *i == '\\') {
638
639                     // url in pandoc style
640                     if ((*i == '[' && strchr(i, ']')) ||
641                         (*i == '!' && *(i + 1) == '[' && strchr(i, ']'))) {
642
643                         if (*i == '!') i++;
644
645                         if (strchr(i, ']')[1] == '(') {
646                             i++;
647
648                             // turn higlighting and underlining on
649                             if (colors)
650                                 wattron(window, COLOR_PAIR(CP_BLUE));
651                             wattron(window, A_UNDERLINE);
652
653                             start_link_name = i;
654
655                             // print the content of the label
656                             // the label is printed as is
657                             do {
658                                 wprintw(window, "%c", *i);
659                                 i++;
660                             } while (*i != ']');
661
662                             length_link_name = i - 1 - start_link_name;
663
664                             i++;
665                             i++;
666
667                             start_url = i;
668
669                             while (*i != ')') i++;
670
671                             url_num = url_add(start_link_name, length_link_name, start_url, i - start_url, 0,0);
672
673                             wprintw(window, " [%d]", url_num);
674
675                             // turn highlighting and underlining off
676                             wattroff(window, A_UNDERLINE);
677                             wattron(window, COLOR_PAIR(CP_WHITE));
678
679                         } else {
680                             wprintw(window, "[");
681                         }
682
683                     } else switch(*i) {
684                         // enable highlight
685                         case '*':
686                             if(colors)
687                                 wattron(window, COLOR_PAIR(CP_RED));
688                             break;
689                         // enable underline
690                         case '_':
691                             wattron(window, A_UNDERLINE);
692                             break;
693                         // enable inline code
694                         case '`':
695                             if(colors)
696                                 wattron(window, COLOR_PAIR(CP_BLACK));
697                             break;
698                         // do nothing for backslashes
699                     }
700
701                     // push special char to stack
702                     (stack->push)(stack, *i);
703
704                 } else {
705                     wprintw(window, "%c", *i);
706                 }
707             }
708
709         } else {
710             // remove backslash from stack
711             if((stack->top)(stack, '\\'))
712                 (stack->pop)(stack);
713
714             // print regular char
715             wprintw(window, "%c", *i);
716         }
717     }
718
719     // pop stack until empty to prevent formated trailing spaces
720     while(!(stack->empty)(stack)) {
721         switch((stack->pop)(stack)) {
722             // disable highlight
723             case '*':
724                 if(colors)
725                     wattron(window, COLOR_PAIR(CP_WHITE));
726                 break;
727             // disable underline
728             case '_':
729                 wattroff(window, A_UNDERLINE);
730                 break;
731             // disable inline code
732             case '`':
733                 if(colors)
734                     wattron(window, COLOR_PAIR(CP_WHITE));
735                 break;
736             // do nothing for backslashes
737         }
738     }
739
740     (stack->delete)(stack);
741 }
742
743 void fade_out(WINDOW *window, int trans, int colors, int invert) {
744     int i; // increment
745     if(colors && COLORS == 256) {
746         for(i = 22; i >= 0; i--) {
747
748             // dim color pairs
749             if(invert) {
750                 init_pair(CP_WHITE, white_ramp_invert[i], trans);
751                 init_pair(CP_BLUE, blue_ramp_invert[i], trans);
752                 init_pair(CP_RED, red_ramp_invert[i], trans);
753                 init_pair(CP_BLACK, 15, white_ramp_invert[i]);
754             } else {
755                 init_pair(CP_WHITE, white_ramp[i], trans);
756                 init_pair(CP_BLUE, blue_ramp[i], trans);
757                 init_pair(CP_RED, red_ramp[i], trans);
758                 init_pair(CP_BLACK, 16, white_ramp[i]);
759             }
760
761             // refresh window with new color
762             wrefresh(window);
763
764             // delay for our eyes to recognize the change
765             usleep(FADE_DELAY);
766         }
767     }
768 }
769
770 void fade_in(WINDOW *window, int trans, int colors, int invert) {
771     int i; // increment
772     if(colors && COLORS == 256) {
773         for(i = 0; i <= 23; i++) {
774
775             // brighten color pairs
776             if(invert) {
777                 init_pair(CP_WHITE, white_ramp_invert[i], trans);
778                 init_pair(CP_BLUE, blue_ramp_invert[i], trans);
779                 init_pair(CP_RED, red_ramp_invert[i], trans);
780                 init_pair(CP_BLACK, 15, white_ramp_invert[i]);
781             } else {
782                 init_pair(CP_WHITE, white_ramp[i], trans);
783                 init_pair(CP_BLUE, blue_ramp[i], trans);
784                 init_pair(CP_RED, red_ramp[i], trans);
785                 init_pair(CP_BLACK, 16, white_ramp[i]);
786             }
787
788             // refresh window with new color
789             wrefresh(window);
790
791             // delay for our eyes to recognize the change
792             usleep(FADE_DELAY);
793         }
794     }
795 }
796
797 int int_length (int val) {
798     int l = 1;
799     while(val > 9) {
800         l++;
801         val /= 10;
802     }
803     return l;
804 }