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