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