added hint to terminal geometry error as suggested in #15
[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                             }
454
455                             // remove top special char from stack
456                             (stack->pop)(stack);
457
458                         // treat special as regular char
459                         } else if((stack->top)(stack, '\\')) {
460                             wprintw(window, "%c", *c);
461
462                             // remove backslash from stack
463                             (stack->pop)(stack);
464
465                         // opening special char
466                         } else {
467                             switch(*c) {
468                                 // enable highlight
469                                 case '*':
470                                     if(colors)
471                                         wattron(window, COLOR_PAIR(CP_RED));
472                                     break;
473                                 // enable underline
474                                 case '_':
475                                     wattron(window, A_UNDERLINE);
476                                     break;
477                                 // do nothing for backslashes
478                             }
479
480                             // push special char to stack
481                             (stack->push)(stack, *c);
482                         }
483
484                     } else {
485                         // remove backslash from stack
486                         if((stack->top)(stack, '\\'))
487                             (stack->pop)(stack);
488
489                         // print regular char
490                         wprintw(window, "%c", *c);
491                     }
492
493                     c++;
494                 }
495
496                 // pop stack until empty to prevent formated trailing spaces
497                 while(!(stack->empty)(stack)) {
498                     switch((stack->pop)(stack)) {
499                         // disable highlight
500                         case '*':
501                             if(colors)
502                                 wattron(window, COLOR_PAIR(CP_WHITE));
503                             break;
504                         // disable underline
505                         case '_':
506                             wattroff(window, A_UNDERLINE);
507                             break;
508                         // do nothing for backslashes
509                     }
510                 }
511             }
512         }
513
514         // fill rest off line with spaces
515         for(i = getcurx(window) - x; i < max_cols; i++)
516             wprintw(window, "%s", " ");
517
518         // reset to default color
519         if(colors)
520             wattron(window, COLOR_PAIR(CP_WHITE));
521         wattroff(window, A_UNDERLINE);
522         wattroff(window, A_REVERSE);
523     }
524
525     (stack->delete)(stack);
526 }
527
528 void fade_out(WINDOW *window, int trans, int colors, int invert) {
529     int i; // increment
530     if(colors) {
531         for(i = 22; i >= 0; i--) {
532
533             // dim color pairs
534             if(invert) {
535                 init_pair(CP_WHITE, white_ramp_invert[i], trans);
536                 init_pair(CP_BLUE, blue_ramp_invert[i], trans);
537                 init_pair(CP_RED, red_ramp_invert[i], trans);
538             } else {
539                 init_pair(CP_WHITE, white_ramp[i], trans);
540                 init_pair(CP_BLUE, blue_ramp[i], trans);
541                 init_pair(CP_RED, red_ramp[i], trans);
542             }
543
544             // refresh window with new color
545             wrefresh(window);
546
547             // delay for our eyes to recognize the change
548             usleep(FADE_DELAY);
549         }
550     }
551 }
552
553 void fade_in(WINDOW *window, int trans, int colors, int invert) {
554     int i; // increment
555     if(colors) {
556         for(i = 0; i <= 23; i++) {
557
558             // brighten color pairs
559             if(invert) {
560                 init_pair(CP_WHITE, white_ramp_invert[i], trans);
561                 init_pair(CP_BLUE, blue_ramp_invert[i], trans);
562                 init_pair(CP_RED, red_ramp_invert[i], trans);
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             }
568
569             // refresh window with new color
570             wrefresh(window);
571
572             // delay for our eyes to recognize the change
573             usleep(FADE_DELAY);
574         }
575     }
576 }
577
578 int int_length (int val) {
579     int l = 1;
580     while(val > 9) {
581         l++;
582         val /= 10;
583     }
584     return l;
585 }