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