cleanup and small change of list spacing in #38 + shortened sample
[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         // IS_UNORDERED_LIST_3
376         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_3)) {
377             offset = next_nonblank(line->text, 0);
378             char format_s[15];
379             strcpy(&format_s[0], CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)? " |  " : "    ");
380             strcpy(&format_s[4], CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)? " |  " : "    ");
381             strcpy(&format_s[8], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_3)? " +- %s" : " `- %s");
382             mvwprintw(window,
383                       y, x,
384                       format_s,
385                       &line->text->text[offset + 2]);
386
387         // IS_UNORDERED_LIST_2
388         } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)) {
389             offset = next_nonblank(line->text, 0);
390             char format_s[11];
391             strcpy(&format_s[0], CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)? " |  " : "    ");
392             strcpy(&format_s[4], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_2)? " +- %s" : " `- %s");
393             mvwprintw(window,
394                       y, x,
395                       format_s,
396                       &line->text->text[offset + 2]);
397
398         // IS_UNORDERED_LIST_1
399         } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)) {
400             offset = next_nonblank(line->text, 0);
401             char format_s[7];
402             strcpy(&format_s[0], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_1)? " +- %s" : " `- %s");
403             mvwprintw(window,
404                       y, x,
405                       format_s,
406                       &line->text->text[offset + 2]);
407
408         // IS_CODE
409         } else if(CHECK_BIT(line->bits, IS_CODE)) {
410
411             // set static offset for code
412             offset = CODE_INDENT;
413
414             // reverse color for code blocks
415             if(colors)
416                 wattron(window, COLOR_PAIR(CP_BLACK));
417
418             // print whole lines
419             mvwprintw(window,
420                       y, x,
421                       "%s", &line->text->text[offset]);
422
423         } else {
424
425             // IS_H1 || IS_H2
426             if(CHECK_BIT(line->bits, IS_H1) || CHECK_BIT(line->bits, IS_H2)) {
427
428                 // set headline color
429                 if(colors)
430                     wattron(window, COLOR_PAIR(CP_BLUE));
431
432                 // enable underline for H1
433                 if(CHECK_BIT(line->bits, IS_H1))
434                     wattron(window, A_UNDERLINE);
435
436                 // skip hashes
437                 while(line->text->text[offset] == '#')
438                     offset = next_word(line->text, offset);
439
440                 // print whole lines
441                 mvwprintw(window,
442                       y, x,
443                       "%s", &line->text->text[offset]);
444
445                 wattroff(window, A_UNDERLINE);
446
447             } else {
448                 // move the cursor in position
449                 wmove(window, y, x);
450
451                 // IS_QUOTE
452                 if(CHECK_BIT(line->bits, IS_QUOTE)) {
453                     while(line->text->text[offset] == '>') {
454                         // print a reverse color block
455                         if(colors) {
456                             wattron(window, COLOR_PAIR(CP_BLACK));
457                             wprintw(window, "%s", " ");
458                             wattron(window, COLOR_PAIR(CP_WHITE));
459                             wprintw(window, "%s", " ");
460                         } else {
461                             wprintw(window, "%s", ">");
462                         }
463
464                         // find next quote or break
465                         offset++;
466                         if(line->text->text[offset] == ' ')
467                             offset = next_word(line->text, offset);
468                     }
469                 }
470
471                 // for each char in line
472                 c = &line->text->text[offset];
473                 while(*c) {
474
475                     // if char is in special char list
476                     if(strchr(special, *c)) {
477
478                         // closing special char (or second backslash)
479                         if((stack->top)(stack, *c)) {
480
481                             switch(*c) {
482                                 // print escaped backslash
483                                 case '\\':
484                                     wprintw(window, "%c", *c);
485                                     break;
486                                 // disable highlight
487                                 case '*':
488                                     if(colors)
489                                         wattron(window, COLOR_PAIR(CP_WHITE));
490                                     break;
491                                 // disable underline
492                                 case '_':
493                                     wattroff(window, A_UNDERLINE);
494                                     break;
495                                 // disable inline code
496                                 case '`':
497                                     if(colors)
498                                         wattron(window, COLOR_PAIR(CP_WHITE));
499                                     break;
500                             }
501
502                             // remove top special char from stack
503                             (stack->pop)(stack);
504
505                         // treat special as regular char
506                         } else if((stack->top)(stack, '\\')) {
507                             wprintw(window, "%c", *c);
508
509                             // remove backslash from stack
510                             (stack->pop)(stack);
511
512                         // opening special char
513                         } else {
514                             switch(*c) {
515                                 // enable highlight
516                                 case '*':
517                                     if(colors)
518                                         wattron(window, COLOR_PAIR(CP_RED));
519                                     break;
520                                 // enable underline
521                                 case '_':
522                                     wattron(window, A_UNDERLINE);
523                                     break;
524                                 // enable inline code
525                                 case '`':
526                                     if(colors)
527                                         wattron(window, COLOR_PAIR(CP_BLACK));
528                                     break;
529                                 // do nothing for backslashes
530                             }
531
532                             // push special char to stack
533                             (stack->push)(stack, *c);
534                         }
535
536                     } else {
537                         // remove backslash from stack
538                         if((stack->top)(stack, '\\'))
539                             (stack->pop)(stack);
540
541                         // print regular char
542                         wprintw(window, "%c", *c);
543                     }
544
545                     c++;
546                 }
547
548                 // pop stack until empty to prevent formated trailing spaces
549                 while(!(stack->empty)(stack)) {
550                     switch((stack->pop)(stack)) {
551                         // disable highlight
552                         case '*':
553                             if(colors)
554                                 wattron(window, COLOR_PAIR(CP_WHITE));
555                             break;
556                         // disable underline
557                         case '_':
558                             wattroff(window, A_UNDERLINE);
559                             break;
560                         // disable inline code
561                         case '`':
562                             if(colors)
563                                 wattron(window, COLOR_PAIR(CP_WHITE));
564                             break;
565                         // do nothing for backslashes
566                     }
567                 }
568             }
569         }
570
571         // fill rest off line with spaces
572         for(i = getcurx(window) - x; i < max_cols; i++)
573             wprintw(window, "%s", " ");
574
575         // reset to default color
576         if(colors)
577             wattron(window, COLOR_PAIR(CP_WHITE));
578         wattroff(window, A_UNDERLINE);
579     }
580
581     (stack->delete)(stack);
582 }
583
584 void fade_out(WINDOW *window, int trans, int colors, int invert) {
585     int i; // increment
586     if(colors && COLORS == 256) {
587         for(i = 22; i >= 0; i--) {
588
589             // dim color pairs
590             if(invert) {
591                 init_pair(CP_WHITE, white_ramp_invert[i], trans);
592                 init_pair(CP_BLUE, blue_ramp_invert[i], trans);
593                 init_pair(CP_RED, red_ramp_invert[i], trans);
594                 init_pair(CP_BLACK, 15, white_ramp_invert[i]);
595             } else {
596                 init_pair(CP_WHITE, white_ramp[i], trans);
597                 init_pair(CP_BLUE, blue_ramp[i], trans);
598                 init_pair(CP_RED, red_ramp[i], trans);
599                 init_pair(CP_BLACK, 16, white_ramp[i]);
600             }
601
602             // refresh window with new color
603             wrefresh(window);
604
605             // delay for our eyes to recognize the change
606             usleep(FADE_DELAY);
607         }
608     }
609 }
610
611 void fade_in(WINDOW *window, int trans, int colors, int invert) {
612     int i; // increment
613     if(colors && COLORS == 256) {
614         for(i = 0; i <= 23; i++) {
615
616             // brighten color pairs
617             if(invert) {
618                 init_pair(CP_WHITE, white_ramp_invert[i], trans);
619                 init_pair(CP_BLUE, blue_ramp_invert[i], trans);
620                 init_pair(CP_RED, red_ramp_invert[i], trans);
621                 init_pair(CP_BLACK, 15, white_ramp_invert[i]);
622             } else {
623                 init_pair(CP_WHITE, white_ramp[i], trans);
624                 init_pair(CP_BLUE, blue_ramp[i], trans);
625                 init_pair(CP_RED, red_ramp[i], trans);
626                 init_pair(CP_BLACK, 16, white_ramp[i]);
627             }
628
629             // refresh window with new color
630             wrefresh(window);
631
632             // delay for our eyes to recognize the change
633             usleep(FADE_DELAY);
634         }
635     }
636 }
637
638 int int_length (int val) {
639     int l = 1;
640     while(val > 9) {
641         l++;
642         val /= 10;
643     }
644     return l;
645 }