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