Merge pull request #38 from FreeBirdLjj/issue#16
[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 <locale.h> // setlocale
25 #include <stdlib.h>
26 #include <string.h> // strchr
27 #include <unistd.h>
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 sc = 1;         // slide count
69     int colors = 0;     // amount of colors supported
70     int fade = 0;       // disable color fading by default
71     int trans = -1;     // enable transparency if term supports it
72     int max_lines = 0;  // max lines per slide
73     int max_cols = 0;   // max columns per line
74     int offset;         // text offset
75
76     // header line 1 is displayed at the top
77     int bar_top = (deck->headers > 0) ? 1 : 0;
78     // header line 2 is displayed at the bottom
79     // anyway we display the slide number at the bottom
80     int bar_bottom = 1;
81
82     slide_t *slide = deck->slide;
83     line_t *line;
84
85     while(slide) {
86         // set max_lines if line count exceeded
87         max_lines = (slide->lines > max_lines) ? slide->lines : max_lines;
88         line = slide->line;
89         while(line) {
90             // set max_cols if length exceeded
91             max_cols = (line->length > max_cols) ? line->length : max_cols;
92             line = line->next;
93         }
94         slide = slide->next;
95     }
96
97     // set locale to display UTF-8 correctly in ncurses
98     setlocale(LC_CTYPE, "");
99
100     // init ncurses
101     initscr();
102
103     if((max_cols > COLS) ||
104        (max_lines + bar_top + bar_bottom + 2 > LINES)) {
105
106         // disable ncurses
107         endwin();
108
109         // print error
110         fprintf(stderr, "Error: Terminal size %ix%i too small. Need at least %ix%i.\n",
111             COLS, LINES, max_cols, max_lines + bar_top + bar_bottom + 2);
112
113         // print hint to solve it
114         if(max_lines + bar_top + bar_bottom + 2 > LINES)
115             fprintf(stderr, "You may need to add additional horizontal rules ('***') to split your file in shorter slides.\n");
116         if(max_cols > COLS)
117             fprintf(stderr, "Automatic line wrapping is not supported yet. You may need to shorten some lines by inserting line breaks.\n");
118
119         return(1);
120     }
121
122     // disable cursor
123     curs_set(0);
124
125     // disable output of keyboard typing
126     noecho();
127
128     // make getch() process one char at a time
129     cbreak();
130
131     // enable arrow keys
132     keypad(stdscr,TRUE);
133
134     // set colors
135     if(has_colors() == TRUE) {
136         start_color();
137         use_default_colors();
138
139         // 256 color mode
140         if(COLORS == 256) {
141
142             if(notrans) {
143                 if(invert) {
144                     trans = 15; // white in 256 color mode
145                 } else {
146                     trans = 16; // black in 256 color mode
147                 }
148             }
149
150             if(invert) {
151                 init_pair(CP_WHITE, 232, trans);
152                 init_pair(CP_BLUE, 21, trans);
153                 init_pair(CP_RED, 196, trans);
154                 init_pair(CP_BLACK, 15, 232);
155             } else {
156                 init_pair(CP_WHITE, 255, trans);
157                 init_pair(CP_BLUE, 123, trans);
158                 init_pair(CP_RED, 213, trans);
159                 init_pair(CP_BLACK, 16, 255);
160             }
161             init_pair(CP_YELLOW, 208, trans);
162
163             // enable color fading
164             if(!nofade) fade = 1;
165
166         // 8 color mode
167         } else {
168
169             if(notrans) {
170                 if(invert) {
171                     trans = 7; // white in 8 color mode
172                 } else {
173                     trans = 0; // black in 8 color mode
174                 }
175             }
176
177             if(invert) {
178                 init_pair(CP_WHITE, 0, trans);
179                 init_pair(CP_BLACK, 7, 0);
180             } else {
181                 init_pair(CP_WHITE, 7, trans);
182                 init_pair(CP_BLACK, 0, 7);
183             }
184             init_pair(CP_BLUE, 4, trans);
185             init_pair(CP_RED, 1, trans);
186             init_pair(CP_YELLOW, 3, trans);
187         }
188
189         colors = 1;
190     }
191
192     // set background color of main window
193     if(colors)
194         wbkgd(stdscr, COLOR_PAIR(CP_YELLOW));
195
196     // setup main window
197     WINDOW *content = newwin(LINES - bar_top - bar_bottom, COLS, 0 + bar_top, 0);
198     if(colors)
199         wbkgd(content, COLOR_PAIR(CP_WHITE));
200
201     slide = deck->slide;
202     while(slide) {
203         // clear windows
204         werase(content);
205         werase(stdscr);
206
207         // always resize window in case terminal geometry has changed
208         wresize(content, LINES - bar_top - bar_bottom, COLS);
209
210         // setup header
211         if(bar_top) {
212             line = deck->header;
213             offset = next_blank(line->text, 0) + 1;
214             // add text to header
215             mvwprintw(stdscr,
216                       0, (COLS - line->length + offset) / 2,
217                       "%s", &line->text->text[offset]);
218         }
219
220         // setup footer
221         if(deck->headers > 1) {
222             line = deck->header->next;
223             offset = next_blank(line->text, 0) + 1;
224             // add text to left footer
225             mvwprintw(stdscr,
226                       LINES - 1, 3,
227                       "%s", &line->text->text[offset]);
228         }
229         // add slide number to right footer
230         mvwprintw(stdscr,
231                   LINES - 1, COLS - int_length(deck->slides) - int_length(sc) - 6,
232                   "%d / %d", sc, deck->slides);
233
234         // make header + fooder visible
235         wrefresh(stdscr);
236
237         line = slide->line;
238         l = 0;
239
240         // print lines
241         while(line) {
242             add_line(content, l, (COLS - max_cols) / 2, line, max_cols, colors);
243             line = line->next;
244             l++;
245         }
246
247         // make content visible
248         wrefresh(content);
249
250         // fade in
251         if(fade)
252             fade_in(content, trans, colors, invert);
253
254         // re-enable fading after any undefined key press
255         if(COLORS == 256 && !nofade) fade = 1;
256
257         // wait for user input
258         c = getch();
259
260         // evaluate user input
261         i = 0;
262         switch(c) {
263
264             // show previous slide
265             case KEY_UP:
266             case KEY_LEFT:
267             case 8:   // BACKSPACE (ascii)
268             case 127: // BACKSPACE (xterm)
269             case 263: // BACKSPACE (getty)
270             case 'h':
271             case 'k':
272                 if(slide->prev) {
273                     slide = slide->prev;
274                     sc--;
275                 } else {
276                     fade = 0;
277                 }
278                 break;
279
280             // show next slide
281             case KEY_DOWN:
282             case KEY_RIGHT:
283             case '\n': // ENTER
284             case ' ':  // SPACE
285             case 'j':
286             case 'l':
287                 if(slide->next) {
288                     slide = slide->next;
289                     sc++;
290                 } else {
291                     fade = 0;
292                 }
293                 break;
294
295             // show slide n
296             case '9': i++;
297             case '8': i++;
298             case '7': i++;
299             case '6': i++;
300             case '5': i++;
301             case '4': i++;
302             case '3': i++;
303             case '2': i++;
304             case '1': i++;
305                 if(i <= deck->slides) {
306                     while(sc != i) {
307                         // search forward
308                         if(sc < i) {
309                             if(slide->next) {
310                                 slide = slide->next;
311                                 sc++;
312                             }
313                         // search backward
314                         } else {
315                             if(slide->prev) {
316                                 slide = slide->prev;
317                                 sc--;
318                             }
319                         }
320                     }
321                 } else {
322                     // disable fading if slide n doesn't exist
323                     fade = 0;
324                 }
325                 break;
326
327             // show first slide
328             case KEY_HOME:
329                 slide = deck->slide;
330                 sc = 1;
331                 break;
332
333             // show last slide
334             case KEY_END:
335                 for(i = sc; i <= deck->slides; i++) {
336                     if(slide->next) {
337                             slide = slide->next;
338                             sc++;
339                     }
340                 }
341                 break;
342
343             // quit
344             case 'q':
345                 // do not fade out on exit
346                 fade = 0;
347                 slide = (void*)0;
348                 break;
349
350             default:
351                 // disable fading on undefined key press
352                 fade = 0;
353                 break;
354         }
355
356         // fade out
357         if(fade)
358             fade_out(content, trans, colors, invert);
359     }
360
361     endwin();
362
363     return(0);
364 }
365
366 void add_line(WINDOW *window, int y, int x, line_t *line, int max_cols, int colors) {
367     int i = 0; // increment
368     char *c; // char pointer for iteration
369     char *special = "\\*_`"; // list of interpreted chars
370     cstack_t *stack = cstack_init();
371     
372     if(line->text->text) {
373         int offset = 0; // text offset
374
375         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_3)) {
376             offset = next_nonblank(line->text, 0);
377             char format_s[15];
378             strcpy(&format_s[0], CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)? "|   " : "    ");
379             strcpy(&format_s[4], CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)? "|   " : "    ");
380             strcpy(&format_s[8], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_3)? "+-- %s" : "`-- %s");
381             mvwprintw(window,
382                       y, x,
383                       format_s,
384                       &line->text->text[offset + 2]);
385         } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)) {
386             offset = next_nonblank(line->text, 0);
387             char format_s[11];
388             strcpy(&format_s[0], CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)? "|   " : "    ");
389             strcpy(&format_s[4], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_2)? "+-- %s" : "`-- %s");
390             mvwprintw(window,
391                       y, x,
392                       format_s,
393                       &line->text->text[offset + 2]);
394         } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)) {
395             offset = next_nonblank(line->text, 0);
396             char format_s[7];
397             strcpy(&format_s[0], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_1)? "+-- %s" : "`-- %s");
398             mvwprintw(window,
399                       y, x,
400                       format_s,
401                       &line->text->text[offset + 2]);
402         } else
403
404
405         // IS_CODE
406         if(CHECK_BIT(line->bits, IS_CODE)) {
407
408             // set static offset for code
409             offset = CODE_INDENT;
410
411             // reverse color for code blocks
412             if(colors)
413                 wattron(window, COLOR_PAIR(CP_BLACK));
414
415             // print whole lines
416             mvwprintw(window,
417                       y, x,
418                       "%s", &line->text->text[offset]);
419
420         } else {
421
422             // IS_H1 || IS_H2
423             if(CHECK_BIT(line->bits, IS_H1) || CHECK_BIT(line->bits, IS_H2)) {
424
425                 // set headline color
426                 if(colors)
427                     wattron(window, COLOR_PAIR(CP_BLUE));
428
429                 // enable underline for H1
430                 if(CHECK_BIT(line->bits, IS_H1))
431                     wattron(window, A_UNDERLINE);
432
433                 // skip hashes
434                 while(line->text->text[offset] == '#')
435                     offset = next_word(line->text, offset);
436
437                 // print whole lines
438                 mvwprintw(window,
439                       y, x,
440                       "%s", &line->text->text[offset]);
441
442                 wattroff(window, A_UNDERLINE);
443
444             } else {
445                 // move the cursor in position
446                 wmove(window, y, x);
447
448                 // IS_QUOTE
449                 if(CHECK_BIT(line->bits, IS_QUOTE)) {
450                     while(line->text->text[offset] == '>') {
451                         // print a reverse color block
452                         if(colors) {
453                             wattron(window, COLOR_PAIR(CP_BLACK));
454                             wprintw(window, "%s", " ");
455                             wattron(window, COLOR_PAIR(CP_WHITE));
456                             wprintw(window, "%s", " ");
457                         } else {
458                             wprintw(window, "%s", ">");
459                         }
460
461                         // find next quote or break
462                         offset++;
463                         if(line->text->text[offset] == ' ')
464                             offset = next_word(line->text, offset);
465                     }
466                 }
467
468                 // for each char in line
469                 c = &line->text->text[offset];
470                 while(*c) {
471
472                     // if char is in special char list
473                     if(strchr(special, *c)) {
474
475                         // closing special char (or second backslash)
476                         if((stack->top)(stack, *c)) {
477
478                             switch(*c) {
479                                 // print escaped backslash
480                                 case '\\':
481                                     wprintw(window, "%c", *c);
482                                     break;
483                                 // disable highlight
484                                 case '*':
485                                     if(colors)
486                                         wattron(window, COLOR_PAIR(CP_WHITE));
487                                     break;
488                                 // disable underline
489                                 case '_':
490                                     wattroff(window, A_UNDERLINE);
491                                     break;
492                                 // disable inline code
493                                 case '`':
494                                     if(colors)
495                                         wattron(window, COLOR_PAIR(CP_WHITE));
496                                     break;
497                             }
498
499                             // remove top special char from stack
500                             (stack->pop)(stack);
501
502                         // treat special as regular char
503                         } else if((stack->top)(stack, '\\')) {
504                             wprintw(window, "%c", *c);
505
506                             // remove backslash from stack
507                             (stack->pop)(stack);
508
509                         // opening special char
510                         } else {
511                             switch(*c) {
512                                 // enable highlight
513                                 case '*':
514                                     if(colors)
515                                         wattron(window, COLOR_PAIR(CP_RED));
516                                     break;
517                                 // enable underline
518                                 case '_':
519                                     wattron(window, A_UNDERLINE);
520                                     break;
521                                 // enable inline code
522                                 case '`':
523                                     if(colors)
524                                         wattron(window, COLOR_PAIR(CP_BLACK));
525                                     break;
526                                 // do nothing for backslashes
527                             }
528
529                             // push special char to stack
530                             (stack->push)(stack, *c);
531                         }
532
533                     } else {
534                         // remove backslash from stack
535                         if((stack->top)(stack, '\\'))
536                             (stack->pop)(stack);
537
538                         // print regular char
539                         wprintw(window, "%c", *c);
540                     }
541
542                     c++;
543                 }
544
545                 // pop stack until empty to prevent formated trailing spaces
546                 while(!(stack->empty)(stack)) {
547                     switch((stack->pop)(stack)) {
548                         // disable highlight
549                         case '*':
550                             if(colors)
551                                 wattron(window, COLOR_PAIR(CP_WHITE));
552                             break;
553                         // disable underline
554                         case '_':
555                             wattroff(window, A_UNDERLINE);
556                             break;
557                         // disable inline code
558                         case '`':
559                             if(colors)
560                                 wattron(window, COLOR_PAIR(CP_WHITE));
561                             break;
562                         // do nothing for backslashes
563                     }
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     (stack->delete)(stack);
579 }
580
581 void fade_out(WINDOW *window, int trans, int colors, int invert) {
582     int i; // increment
583     if(colors && COLORS == 256) {
584         for(i = 22; i >= 0; i--) {
585
586             // dim color pairs
587             if(invert) {
588                 init_pair(CP_WHITE, white_ramp_invert[i], trans);
589                 init_pair(CP_BLUE, blue_ramp_invert[i], trans);
590                 init_pair(CP_RED, red_ramp_invert[i], trans);
591                 init_pair(CP_BLACK, 15, white_ramp_invert[i]);
592             } else {
593                 init_pair(CP_WHITE, white_ramp[i], trans);
594                 init_pair(CP_BLUE, blue_ramp[i], trans);
595                 init_pair(CP_RED, red_ramp[i], trans);
596                 init_pair(CP_BLACK, 16, white_ramp[i]);
597             }
598
599             // refresh window with new color
600             wrefresh(window);
601
602             // delay for our eyes to recognize the change
603             usleep(FADE_DELAY);
604         }
605     }
606 }
607
608 void fade_in(WINDOW *window, int trans, int colors, int invert) {
609     int i; // increment
610     if(colors && COLORS == 256) {
611         for(i = 0; i <= 23; i++) {
612
613             // brighten color pairs
614             if(invert) {
615                 init_pair(CP_WHITE, white_ramp_invert[i], trans);
616                 init_pair(CP_BLUE, blue_ramp_invert[i], trans);
617                 init_pair(CP_RED, red_ramp_invert[i], trans);
618                 init_pair(CP_BLACK, 15, white_ramp_invert[i]);
619             } else {
620                 init_pair(CP_WHITE, white_ramp[i], trans);
621                 init_pair(CP_BLUE, blue_ramp[i], trans);
622                 init_pair(CP_RED, red_ramp[i], trans);
623                 init_pair(CP_BLACK, 16, white_ramp[i]);
624             }
625
626             // refresh window with new color
627             wrefresh(window);
628
629             // delay for our eyes to recognize the change
630             usleep(FADE_DELAY);
631         }
632     }
633 }
634
635 int int_length (int val) {
636     int l = 1;
637     while(val > 9) {
638         l++;
639         val /= 10;
640     }
641     return l;
642 }