bfc753d9d8697bd2673221a3f8bf731d5bcd0036
[smdp.git] / src / viewer.c
1 /*
2  * Functions necessary to display a deck of slides in different color modes
3  * using ncurses.
4  * Copyright (C) 2018 Michael Goehler
5  *
6  * This file is part of mdp.
7  *
8  * This program is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program. If not, see <http://www.gnu.org/licenses/>.
20  *
21  */
22
23 #include <ctype.h>  // isalnum
24 #include <wchar.h>  // wcschr
25 #include <wctype.h> // iswalnum
26 #include <string.h> // strcpy
27 #include <unistd.h> // usleep
28 #include <stdlib.h> // getenv
29 #include "viewer.h"
30 #include "config.h"
31
32 int ncurses_display(deck_t *deck, int reload, int noreload, int slidenum, int nocodebg) {
33
34     int c = 0;                // char
35     int i = 0;                // iterate
36     int l = 0;                // line number
37     int lc = 0;               // line count
38     int sc = 1;               // slide count
39     int colors = 0;           // amount of colors supported
40     int max_lines = 0;        // max lines per slide
41     int max_lines_slide = -1; // the slide that has the most lines
42     int max_cols = 0;         // max columns per line
43     int offset;               // text offset
44     int stop = 0;             // passed stop bits per slide
45
46     // header line 1 is displayed at the top
47     int bar_top = (deck->headers > 0) ? 1 : 0;
48     // header line 2 is displayed at the bottom
49     // anyway we display the slide number at the bottom
50     int bar_bottom = (slidenum || deck->headers > 1)? 1 : 0;
51
52     slide_t *slide = deck->slide;
53     line_t *line;
54
55     // init ncurses
56     initscr();
57
58     while(slide) {
59         lc = 0;
60         line = slide->line;
61
62         while(line && line->text) {
63
64             if (line->text->value) {
65                 lc += url_count_inline(line->text->value);
66                 line->length -= url_len_inline(line->text->value);
67             }
68
69             if(line->length > COLS) {
70                 i = line->length;
71                 offset = 0;
72                 while(i > COLS) {
73
74                     i = prev_blank(line->text, offset + COLS) - offset;
75
76                     // single word is > COLS
77                     if(!i) {
78                         // calculate min_width
79                         i = next_blank(line->text, offset + COLS) - offset;
80
81                         // disable ncurses
82                         endwin();
83
84                         // print error
85                         fwprintf(stderr, L"Error: Terminal width (%i columns) too small. Need at least %i columns.\n", COLS, i);
86                         fwprintf(stderr, L"You may need to shorten some lines by inserting line breaks.\n");
87
88                         // no reload
89                         return 0;
90                     }
91
92                     // set max_cols
93                     max_cols = MAX(i, max_cols);
94
95                     // iterate to next line
96                     offset = prev_blank(line->text, offset + COLS);
97                     i = line->length - offset;
98                     lc++;
99                 }
100                 // set max_cols one last time
101                 max_cols = MAX(i, max_cols);
102             } else {
103                 // set max_cols
104                 max_cols = MAX(line->length, max_cols);
105             }
106             lc++;
107             line = line->next;
108         }
109
110         max_lines = MAX(lc, max_lines);
111         if (lc == max_lines) {
112             max_lines_slide = sc;
113         }
114
115         slide->lines_consumed = lc;
116         slide = slide->next;
117         ++sc;
118     }
119
120     // not enough lines
121     if(max_lines + bar_top + bar_bottom > LINES) {
122
123         // disable ncurses
124         endwin();
125
126         // print error
127         fwprintf(stderr, L"Error: Terminal height (%i lines) too small. Need at least %i lines for slide #%i.\n", LINES, max_lines + bar_top + bar_bottom, max_lines_slide);
128         fwprintf(stderr, L"You may need to add additional horizontal rules (---) to split your file in shorter slides.\n");
129
130         // no reload
131         return 0;
132     }
133
134     // disable cursor
135     curs_set(0);
136
137     // disable output of keyboard typing
138     noecho();
139
140     // make getch() process one char at a time
141     cbreak();
142
143     // enable arrow keys
144     keypad(stdscr,TRUE);
145
146     // set colors
147     if(has_colors() == TRUE) {
148         start_color();
149         use_default_colors();
150
151         init_pair(CP_FG, FG_COLOR, BG_COLOR);
152         init_pair(CP_BG, BG_COLOR, FG_COLOR);
153         init_pair(CP_HEADER, HEADER_COLOR, BG_COLOR);
154         init_pair(CP_BOLD, BOLD_COLOR, BG_COLOR);
155         init_pair(CP_TITLE, TITLE_COLOR, BG_COLOR);
156
157         colors = 1;
158     }
159
160     // set background color for main window
161     if(colors)
162         wbkgd(stdscr, COLOR_PAIR(CP_FG));
163
164     // setup content window
165     WINDOW *content = newwin(LINES - bar_top - bar_bottom, COLS, 0 + bar_top, 0);
166
167     // set background color of content window
168     if(colors)
169         wbkgd(content, COLOR_PAIR(CP_FG));
170
171     slide = deck->slide;
172
173     // find slide to reload
174     sc = 1;
175     while(reload > 1 && reload <= deck->slides) {
176         slide = slide->next;
177         sc++;
178         reload--;
179     }
180
181     // reset reload indicator
182     reload = 0;
183
184     while(slide) {
185
186         url_init();
187
188         // clear windows
189         werase(content);
190         werase(stdscr);
191
192         // always resize window in case terminal geometry has changed
193         wresize(content, LINES - bar_top - bar_bottom, COLS);
194
195         // set main window text color
196         if(colors)
197             wattron(stdscr, COLOR_PAIR(CP_TITLE));
198
199         // setup header
200         if(bar_top) {
201             line = deck->header;
202             offset = next_blank(line->text, 0) + 1;
203             // add text to header
204             mvwaddwstr(stdscr,
205                        0, (COLS - line->length + offset) / 2,
206                        &line->text->value[offset]);
207         }
208
209         // setup footer
210         if(deck->headers > 1) {
211             line = deck->header->next;
212             offset = next_blank(line->text, 0) + 1;
213             switch(slidenum) {
214                 case 0: // add text to center footer
215                     mvwaddwstr(stdscr,
216                                LINES - 1, (COLS - line->length + offset) / 2,
217                                &line->text->value[offset]);
218                     break;
219                 case 1:
220                 case 2: // add text to left footer
221                     mvwaddwstr(stdscr,
222                                LINES - 1, 3,
223                                &line->text->value[offset]);
224                     break;
225             }
226         }
227
228         // add slide number to right footer
229         switch(slidenum) {
230             case 1: // show slide number only
231                 mvwprintw(stdscr,
232                           LINES - 1, COLS - int_length(sc) - 3,
233                           "%d", sc);
234                 break;
235             case 2: // show current slide & number of slides
236                 mvwprintw(stdscr,
237                           LINES - 1, COLS - int_length(deck->slides) - int_length(sc) - 6,
238                           "%d / %d", sc, deck->slides);
239                 break;
240         }
241
242         // copy changed lines in main window to virtual screen
243         wnoutrefresh(stdscr);
244
245         line = slide->line;
246         l = stop = 0;
247
248         // print lines
249         while(line) {
250             add_line(content, l + ((LINES - slide->lines_consumed - bar_top - bar_bottom) / 2),
251                      (COLS - max_cols) / 2, line, max_cols, colors, nocodebg);
252
253             // raise stop counter if we pass a line having a stop bit
254             if(CHECK_BIT(line->bits, IS_STOP))
255                 stop++;
256
257             l += (line->length / COLS) + 1;
258             line = line->next;
259
260             // only stop here if we didn't stop here recently
261             if(stop > slide->stop)
262                 break;
263         }
264
265         // print pandoc URL references
266         // only if we already printed all lines of the current slide (or output is stopped)
267         if(!line ||
268            stop > slide->stop) {
269             int i, ymax;
270             getmaxyx( content, ymax, i );
271             for (i = 0; i < url_get_amount(); i++) {
272                 mvwprintw(content, ymax - url_get_amount() - 1 + i, 3,
273                           "[%d] ", i);
274                 waddwstr(content, url_get_target(i));
275             }
276         }
277
278         // copy changed lines in content window to virtual screen
279         wnoutrefresh(content);
280
281         // compare virtual screen to physical screen and does the actual updates
282         doupdate();
283
284         // wait for user input
285         c = getch();
286
287         // evaluate user input
288         i = 0;
289
290         if (evaluate_binding(prev_slide_binding, c)) {
291             // show previous slide or stop bit
292             if(stop > 1 || (stop == 1 && !line)) {
293                 // show current slide again
294                 // but stop one stop bit earlier
295                 slide->stop--;
296             } else {
297                 if(slide->prev) {
298                     // show previous slide
299                     slide = slide->prev;
300                     sc--;
301                     //stop on first bullet point always
302                     if(slide->stop > 0)
303                         slide->stop = 0;
304                 }
305             }
306         } else if (evaluate_binding(next_slide_binding, c)) {
307             // show next slide or stop bit
308             if(stop && line) {
309                 // show current slide again
310                 // but stop one stop bit later (or at end of slide)
311                 slide->stop++;
312             } else {
313                 if(slide->next) {
314                     // show next slide
315                     slide = slide->next;
316                     sc++;
317                 }
318             }
319         } else if (isdigit(c) && c != '0') {
320             // show slide n
321             i = get_slide_number(c);
322             if(i > 0 && i <= deck->slides) {
323                 while(sc != i) {
324                     // search forward
325                     if(sc < i) {
326                         if(slide->next) {
327                             slide = slide->next;
328                             sc++;
329                         }
330                     // search backward
331                     } else {
332                         if(slide->prev) {
333                             slide = slide->prev;
334                             sc--;
335                         }
336                     }
337                 }
338             }        
339         } else if (evaluate_binding(first_slide_binding, c)) {
340             // show first slide
341             slide = deck->slide;
342             sc = 1;
343         } else if (evaluate_binding(last_slide_binding, c)) {
344             // show last slide
345             for(i = sc; i <= deck->slides; i++) {
346                 if(slide->next) {
347                         slide = slide->next;
348                         sc++;
349                 }
350             }
351         } else if (evaluate_binding(reload_binding, c)) {
352             // reload
353             if(noreload == 0) {
354                 // reload slide N
355                 reload = sc;
356                 slide = NULL;
357             }
358         } else if (evaluate_binding(quit_binding, c)) {
359             // quit
360             // do not reload
361             reload = 0;
362             slide = NULL;
363         }
364
365         url_purge();
366     }
367
368     // disable ncurses
369     endwin();
370
371     // free ncurses memory
372     delwin(content);
373     if(reload == 0)
374         delwin(stdscr);
375
376     // return reload indicator (0 means no reload)
377     return reload;
378 }
379
380 void setup_list_strings(void)
381 {
382     const char *str;
383
384     /* utf8 can require 6 bytes */
385     if ((str = getenv("MDP_LIST_OPEN")) != NULL && strlen(str) <= 4*6) {
386         list_open1 = list_open2 = list_open3 = str;
387     } else {
388         if ((str = getenv("MDP_LIST_OPEN1")) != NULL && strlen(str) <= 4*6)
389             list_open1 = str;
390         if ((str = getenv("MDP_LIST_OPEN2")) != NULL && strlen(str) <= 4*6)
391             list_open2 = str;
392         if ((str = getenv("MDP_LIST_OPEN3")) != NULL && strlen(str) <= 4*6)
393             list_open3 = str;
394     }
395     if ((str = getenv("MDP_LIST_HEAD")) != NULL && strlen(str) <= 4*6) {
396         list_head1 = list_head2 = list_head3 = str;
397     } else {
398         if ((str = getenv("MDP_LIST_HEAD1")) != NULL && strlen(str) <= 4*6)
399             list_head1 = str;
400         if ((str = getenv("MDP_LIST_HEAD2")) != NULL && strlen(str) <= 4*6)
401             list_head2 = str;
402         if ((str = getenv("MDP_LIST_HEAD3")) != NULL && strlen(str) <= 4*6)
403             list_head3 = str;
404     }
405 }
406
407 void add_line(WINDOW *window, int y, int x, line_t *line, int max_cols, int colors, int nocodebg) {
408
409     int i; // increment
410     int offset = 0; // text offset
411
412     // move the cursor in position
413     wmove(window, y, x);
414
415     if(!line->text->value) {
416
417         // fill rest off line with spaces if we are in a code block
418         if(CHECK_BIT(line->bits, IS_CODE) && colors) {
419             if(colors && !nocodebg)
420                 wattron(window, COLOR_PAIR(CP_BG));
421             for(i = getcurx(window) - x; i < max_cols; i++)
422                 wprintw(window, "%s", " ");
423         }
424
425         // do nothing
426         return;
427     }
428
429     // IS_UNORDERED_LIST_3
430     if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_3)) {
431         offset = next_nonblank(line->text, 0);
432         char prompt[13 * 6];
433         int pos = 0, len, cnt;
434         len = sizeof(prompt) - pos;
435         cnt = snprintf(&prompt[pos], len, "%s", CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)? list_open1 : "    ");
436         pos += (cnt > len - 1 ? len - 1 : cnt);
437         len = sizeof(prompt) - pos;
438         cnt = snprintf(&prompt[pos], len, "%s", CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)? list_open2 : "    ");
439         pos += (cnt > len - 1 ? len - 1 : cnt);
440         len = sizeof(prompt) - pos;
441
442         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_EXT)) {
443             snprintf(&prompt[pos], len, "%s", line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_3)? list_open3 : "    ");
444         } else {
445             snprintf(&prompt[pos], len, "%s", list_head3);
446             offset += 2;
447         }
448
449         wprintw(window,
450                 "%s", prompt);
451
452         if(!CHECK_BIT(line->bits, IS_CODE))
453             inline_display(window, &line->text->value[offset], colors, nocodebg);
454
455     // IS_UNORDERED_LIST_2
456     } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)) {
457         offset = next_nonblank(line->text, 0);
458         char prompt[9 * 6];
459         int pos = 0, len, cnt;
460         len = sizeof(prompt) - pos;
461         cnt = snprintf(&prompt[pos], len, "%s", CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)? list_open1 : "    ");
462         pos += (cnt > len - 1 ? len - 1 : cnt);
463         len = sizeof(prompt) - pos;
464
465         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_EXT)) {
466             snprintf(&prompt[pos], len, "%s", line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_2)? list_open2 : "    ");
467         } else {
468             snprintf(&prompt[pos], len, "%s", list_head2);
469             offset += 2;
470         }
471
472         wprintw(window,
473                 "%s", prompt);
474
475         if(!CHECK_BIT(line->bits, IS_CODE))
476             inline_display(window, &line->text->value[offset], colors, nocodebg);
477
478     // IS_UNORDERED_LIST_1
479     } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)) {
480         offset = next_nonblank(line->text, 0);
481         char prompt[5 * 6];
482
483         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_EXT)) {
484             strcpy(&prompt[0], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_1)? list_open1 : "    ");
485         } else {
486             strcpy(&prompt[0], list_head1);
487             offset += 2;
488         }
489
490         wprintw(window,
491                 "%s", prompt);
492
493         if(!CHECK_BIT(line->bits, IS_CODE))
494             inline_display(window, &line->text->value[offset], colors, nocodebg);
495     }
496
497     // IS_CODE
498     if(CHECK_BIT(line->bits, IS_CODE)) {
499
500         if (!CHECK_BIT(line->bits, IS_TILDE_CODE) &&
501             !CHECK_BIT(line->bits, IS_GFM_CODE)) {
502             // set static offset for code
503             offset = CODE_INDENT;
504         }
505
506         // reverse color for code blocks
507         if(colors && !nocodebg)
508             wattron(window, COLOR_PAIR(CP_BG));
509
510         // print whole lines
511         waddwstr(window, &line->text->value[offset]);
512     }
513
514     if(!CHECK_BIT(line->bits, IS_UNORDERED_LIST_1) &&
515        !CHECK_BIT(line->bits, IS_UNORDERED_LIST_2) &&
516        !CHECK_BIT(line->bits, IS_UNORDERED_LIST_3) &&
517        !CHECK_BIT(line->bits, IS_CODE)) {
518
519         // IS_QUOTE
520         if(CHECK_BIT(line->bits, IS_QUOTE)) {
521             while(line->text->value[offset] == '>') {
522                 // print a reverse color block
523                 if(colors) {
524                     wattron(window, COLOR_PAIR(CP_BG));
525                     wprintw(window, "%s", " ");
526                     wattron(window, COLOR_PAIR(CP_FG));
527                     wprintw(window, "%s", " ");
528                 } else {
529                     wprintw(window, "%s", ">");
530                 }
531
532                 // find next quote or break
533                 offset++;
534                 if(line->text->value[offset] == ' ')
535                     offset = next_word(line->text, offset);
536             }
537
538             inline_display(window, &line->text->value[offset], colors, nocodebg);
539         } else {
540
541             // IS_CENTER
542             if(CHECK_BIT(line->bits, IS_CENTER)) {
543                 if(line->length < max_cols) {
544                     wmove(window, y, x + ((max_cols - line->length) / 2));
545                 }
546             }
547
548             // IS_H1 || IS_H2
549             if(CHECK_BIT(line->bits, IS_H1) || CHECK_BIT(line->bits, IS_H2)) {
550
551                 // set headline color
552                 if(colors)
553                     wattron(window, COLOR_PAIR(CP_HEADER));
554
555                 // enable underline for H1
556                 if(CHECK_BIT(line->bits, IS_H1))
557                     wattron(window, A_UNDERLINE);
558
559                 // skip hashes
560                 while(line->text->value[offset] == '#')
561                     offset = next_word(line->text, offset);
562
563                 // print whole lines
564                 waddwstr(window, &line->text->value[offset]);
565
566                 wattroff(window, A_UNDERLINE);
567
568             // no line-wide markdown
569             } else {
570
571                 inline_display(window, &line->text->value[offset], colors, nocodebg);
572             }
573         }
574     }
575
576     // reset to default color
577     if(colors)
578         wattron(window, COLOR_PAIR(CP_FG));
579     wattroff(window, A_UNDERLINE);
580 }
581
582 void inline_display(WINDOW *window, const wchar_t *c, const int colors, int nocodebg) {
583     const static wchar_t *special = L"\\*_`!["; // list of interpreted chars
584     const wchar_t *i = c; // iterator
585     const wchar_t *start_link_name, *start_url;
586     int length_link_name, url_num;
587     cstack_t *stack = cstack_init();
588
589
590     // for each char in line
591     for(; *i; i++) {
592
593         // if char is in special char list
594         if(wcschr(special, *i)) {
595
596             // closing special char (or second backslash)
597             // only if not followed by :alnum:
598             if((stack->top)(stack, *i) &&
599                (!iswalnum(i[1]) || *(i + 1) == L'\0' || *i == L'\\')) {
600
601                 switch(*i) {
602                     // print escaped backslash
603                     case L'\\':
604                         waddnwstr(window, i, 1);
605                         break;
606                     // disable highlight
607                     case L'*':
608                         if(colors)
609                             wattron(window, COLOR_PAIR(CP_FG));
610                         break;
611                     // disable underline
612                     case L'_':
613                         wattroff(window, A_UNDERLINE);
614                         break;
615                     // disable inline code
616                     case L'`':
617                         if(colors)
618                             wattron(window, COLOR_PAIR(CP_FG));
619                         break;
620                 }
621
622                 // remove top special char from stack
623                 (stack->pop)(stack);
624
625             // treat special as regular char
626             } else if((stack->top)(stack, L'\\')) {
627                 waddnwstr(window, i, 1);
628
629                 // remove backslash from stack
630                 (stack->pop)(stack);
631
632             // opening special char
633             } else {
634
635                 // emphasis or code span can start after new-line or space only
636                 // and of cause after another emphasis markup
637                 //TODO this condition looks ugly
638                 if(i == c ||
639                    iswspace(*(i - 1)) ||
640                    ((iswspace(*(i - 1)) || *(i - 1) == L'*' || *(i - 1) == L'_') &&
641                     ((i - 1) == c || iswspace(*(i - 2)))) ||
642                    *i == L'\\') {
643
644                     // url in pandoc style
645                     if ((*i == L'[' && wcschr(i, L']')) ||
646                         (*i == L'!' && *(i + 1) == L'[' && wcschr(i, L']'))) {
647
648                         if (*i == L'!') i++;
649
650                         if (wcschr(i, L']')[1] == L'(' && wcschr(i, L')')) {
651                             i++;
652
653                             // turn higlighting and underlining on
654                             if (colors)
655                                 wattron(window, COLOR_PAIR(CP_HEADER));
656                             wattron(window, A_UNDERLINE);
657
658                             start_link_name = i;
659
660                             // print the content of the label
661                             // the label is printed as is
662                             do {
663                                 waddnwstr(window, i, 1);
664                                 i++;
665                             } while (*i != L']');
666
667                             length_link_name = i - 1 - start_link_name;
668
669                             i++;
670                             i++;
671
672                             start_url = i;
673
674                             while (*i != L')') i++;
675
676                             url_num = url_add(start_link_name, length_link_name, start_url, i - start_url, 0, 0);
677
678                             wprintw(window, " [%d]", url_num);
679
680                             // turn highlighting and underlining off
681                             wattroff(window, A_UNDERLINE);
682                             wattron(window, COLOR_PAIR(CP_FG));
683
684                         } else {
685                             wprintw(window, "[");
686                         }
687
688                     } else switch(*i) {
689                         // enable highlight
690                         case L'*':
691                             if(colors)
692                                 wattron(window, COLOR_PAIR(CP_BOLD));
693                             break;
694                         // enable underline
695                         case L'_':
696                             wattron(window, A_UNDERLINE);
697                             break;
698                         // enable inline code
699                         case L'`':
700                             if(colors && !nocodebg)
701                                 wattron(window, COLOR_PAIR(CP_BG));
702                             break;
703                         // do nothing for backslashes
704                     }
705
706                     // push special char to stack
707                     (stack->push)(stack, *i);
708
709                 } else {
710                     waddnwstr(window, i, 1);
711                 }
712             }
713
714         } else {
715             // remove backslash from stack
716             if((stack->top)(stack, L'\\'))
717                 (stack->pop)(stack);
718
719             // print regular char
720             waddnwstr(window, i, 1);
721         }
722     }
723
724     // pop stack until empty to prevent formated trailing spaces
725     while(!(stack->empty)(stack)) {
726         switch((stack->pop)(stack)) {
727             // disable highlight
728             case L'*':
729                 if(colors)
730                     wattron(window, COLOR_PAIR(CP_FG));
731                 break;
732             // disable underline
733             case L'_':
734                 wattroff(window, A_UNDERLINE);
735                 break;
736             // disable inline code
737             case L'`':
738                 if(colors)
739                     wattron(window, COLOR_PAIR(CP_FG));
740                 break;
741             // do nothing for backslashes
742         }
743     }
744
745     (stack->delete)(stack);
746 }
747
748 int int_length (int val) {
749     int l = 1;
750     while(val > 9) {
751         l++;
752         val /= 10;
753     }
754     return l;
755 }
756
757 int get_slide_number(char init) {
758     int retval = init - '0';
759     int c;
760     // block for tenths of a second when using getch, ERR if no input
761     halfdelay(GOTO_SLIDE_DELAY);
762     while((c = getch()) != ERR) {
763         if (c < '0' || c > '9') {
764             retval = -1;
765             break;
766         }
767         retval = (retval * 10) + (c - '0');
768     }
769     nocbreak();     // cancel half delay mode
770     cbreak();       // go back to cbreak
771     return retval;
772 }
773
774 bool evaluate_binding(const int bindings[], char c) {
775     int binding;
776     int ind = 0; 
777     while((binding = bindings[ind]) != 0) {
778         if (c == binding) return true;
779         ind++;
780     }
781     return false;
782 }
783