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