The calculation of `max_lines` takes in account the additionnal lines from URLs
[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                         if (strchr(i, ']')[1] == '(') {
642                             i++;
643
644                             // turn higlighting and underlining on
645                             if (colors)
646                                 wattron(window, COLOR_PAIR(CP_BLUE));
647                             wattron(window, A_UNDERLINE);
648
649                             start_link_name = i;
650
651                             // print the content of the label
652                             // the label is printed as is
653                             do {
654                                 wprintw(window, "%c", *i);
655                                 i++;
656                             } while (*i != ']');
657
658                             length_link_name = i - 1 - start_link_name;
659
660                             i++;
661                             i++;
662
663                             start_url = i;
664
665                             while (*i != ')') i++;
666
667                             url_num = url_add(start_link_name, length_link_name, start_url, i - start_url, 0,0);
668
669                             wprintw(window, "[%d]", url_num);
670
671                             // turn highlighting and underlining off
672                             wattroff(window, A_UNDERLINE);
673                             wattron(window, COLOR_PAIR(CP_WHITE));
674
675                         } else {
676                             wprintw(window, "[");
677                         }
678
679                     } else switch(*i) {
680                         // enable highlight
681                         case '*':
682                             if(colors)
683                                 wattron(window, COLOR_PAIR(CP_RED));
684                             break;
685                         // enable underline
686                         case '_':
687                             wattron(window, A_UNDERLINE);
688                             break;
689                         // enable inline code
690                         case '`':
691                             if(colors)
692                                 wattron(window, COLOR_PAIR(CP_BLACK));
693                             break;
694                         // do nothing for backslashes
695                     }
696
697                     // push special char to stack
698                     (stack->push)(stack, *i);
699
700                 } else {
701                     wprintw(window, "%c", *i);
702                 }
703             }
704
705         } else {
706             // remove backslash from stack
707             if((stack->top)(stack, '\\'))
708                 (stack->pop)(stack);
709
710             // print regular char
711             wprintw(window, "%c", *i);
712         }
713     }
714
715     // pop stack until empty to prevent formated trailing spaces
716     while(!(stack->empty)(stack)) {
717         switch((stack->pop)(stack)) {
718             // disable highlight
719             case '*':
720                 if(colors)
721                     wattron(window, COLOR_PAIR(CP_WHITE));
722                 break;
723             // disable underline
724             case '_':
725                 wattroff(window, A_UNDERLINE);
726                 break;
727             // disable inline code
728             case '`':
729                 if(colors)
730                     wattron(window, COLOR_PAIR(CP_WHITE));
731                 break;
732             // do nothing for backslashes
733         }
734     }
735
736     (stack->delete)(stack);
737 }
738
739 void fade_out(WINDOW *window, int trans, int colors, int invert) {
740     int i; // increment
741     if(colors && COLORS == 256) {
742         for(i = 22; i >= 0; i--) {
743
744             // dim color pairs
745             if(invert) {
746                 init_pair(CP_WHITE, white_ramp_invert[i], trans);
747                 init_pair(CP_BLUE, blue_ramp_invert[i], trans);
748                 init_pair(CP_RED, red_ramp_invert[i], trans);
749                 init_pair(CP_BLACK, 15, white_ramp_invert[i]);
750             } else {
751                 init_pair(CP_WHITE, white_ramp[i], trans);
752                 init_pair(CP_BLUE, blue_ramp[i], trans);
753                 init_pair(CP_RED, red_ramp[i], trans);
754                 init_pair(CP_BLACK, 16, white_ramp[i]);
755             }
756
757             // refresh window with new color
758             wrefresh(window);
759
760             // delay for our eyes to recognize the change
761             usleep(FADE_DELAY);
762         }
763     }
764 }
765
766 void fade_in(WINDOW *window, int trans, int colors, int invert) {
767     int i; // increment
768     if(colors && COLORS == 256) {
769         for(i = 0; i <= 23; i++) {
770
771             // brighten color pairs
772             if(invert) {
773                 init_pair(CP_WHITE, white_ramp_invert[i], trans);
774                 init_pair(CP_BLUE, blue_ramp_invert[i], trans);
775                 init_pair(CP_RED, red_ramp_invert[i], trans);
776                 init_pair(CP_BLACK, 15, white_ramp_invert[i]);
777             } else {
778                 init_pair(CP_WHITE, white_ramp[i], trans);
779                 init_pair(CP_BLUE, blue_ramp[i], trans);
780                 init_pair(CP_RED, red_ramp[i], trans);
781                 init_pair(CP_BLACK, 16, white_ramp[i]);
782             }
783
784             // refresh window with new color
785             wrefresh(window);
786
787             // delay for our eyes to recognize the change
788             usleep(FADE_DELAY);
789         }
790     }
791 }
792
793 int int_length (int val) {
794     int l = 1;
795     while(val > 9) {
796         l++;
797         val /= 10;
798     }
799     return l;
800 }