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