color config for code blocks
[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) {
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_HEADER, HEADER_COLOR, BG_COLOR);
153         init_pair(CP_BOLD, BOLD_COLOR, BG_COLOR);
154         init_pair(CP_TITLE, TITLE_COLOR, BG_COLOR);
155         init_pair(CP_CODE, CODEFG_COLOR, CODEBG_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);
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) {
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             wattron(window, COLOR_PAIR(CP_CODE));
420             for(i = getcurx(window) - x; i < max_cols; i++)
421                 wprintw(window, "%s", " ");
422         }
423
424         // do nothing
425         return;
426     }
427
428     // IS_UNORDERED_LIST_3
429     if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_3)) {
430         offset = next_nonblank(line->text, 0);
431         char prompt[13 * 6];
432         int pos = 0, len, cnt;
433         len = sizeof(prompt) - pos;
434         cnt = snprintf(&prompt[pos], len, "%s", CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)? list_open1 : "    ");
435         pos += (cnt > len - 1 ? len - 1 : cnt);
436         len = sizeof(prompt) - pos;
437         cnt = snprintf(&prompt[pos], len, "%s", CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)? list_open2 : "    ");
438         pos += (cnt > len - 1 ? len - 1 : cnt);
439         len = sizeof(prompt) - pos;
440
441         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_EXT)) {
442             snprintf(&prompt[pos], len, "%s", line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_3)? list_open3 : "    ");
443         } else {
444             snprintf(&prompt[pos], len, "%s", list_head3);
445             offset += 2;
446         }
447
448         wprintw(window,
449                 "%s", prompt);
450
451         if(!CHECK_BIT(line->bits, IS_CODE))
452             inline_display(window, &line->text->value[offset], colors);
453
454     // IS_UNORDERED_LIST_2
455     } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)) {
456         offset = next_nonblank(line->text, 0);
457         char prompt[9 * 6];
458         int pos = 0, len, cnt;
459         len = sizeof(prompt) - pos;
460         cnt = snprintf(&prompt[pos], len, "%s", CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)? list_open1 : "    ");
461         pos += (cnt > len - 1 ? len - 1 : cnt);
462         len = sizeof(prompt) - pos;
463
464         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_EXT)) {
465             snprintf(&prompt[pos], len, "%s", line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_2)? list_open2 : "    ");
466         } else {
467             snprintf(&prompt[pos], len, "%s", list_head2);
468             offset += 2;
469         }
470
471         wprintw(window,
472                 "%s", prompt);
473
474         if(!CHECK_BIT(line->bits, IS_CODE))
475             inline_display(window, &line->text->value[offset], colors);
476
477     // IS_UNORDERED_LIST_1
478     } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)) {
479         offset = next_nonblank(line->text, 0);
480         char prompt[5 * 6];
481
482         if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_EXT)) {
483             strcpy(&prompt[0], line->next && CHECK_BIT(line->next->bits, IS_UNORDERED_LIST_1)? list_open1 : "    ");
484         } else {
485             strcpy(&prompt[0], list_head1);
486             offset += 2;
487         }
488
489         wprintw(window,
490                 "%s", prompt);
491
492         if(!CHECK_BIT(line->bits, IS_CODE))
493             inline_display(window, &line->text->value[offset], colors);
494     }
495
496     // IS_CODE
497     if(CHECK_BIT(line->bits, IS_CODE)) {
498
499         if (!CHECK_BIT(line->bits, IS_TILDE_CODE) &&
500             !CHECK_BIT(line->bits, IS_GFM_CODE)) {
501             // set static offset for code
502             offset = CODE_INDENT;
503         }
504
505         // color for code block
506         if (colors)
507         wattron(window, COLOR_PAIR(CP_CODE));
508
509         // print whole lines
510         waddwstr(window, &line->text->value[offset]);
511     }
512
513     if(!CHECK_BIT(line->bits, IS_UNORDERED_LIST_1) &&
514        !CHECK_BIT(line->bits, IS_UNORDERED_LIST_2) &&
515        !CHECK_BIT(line->bits, IS_UNORDERED_LIST_3) &&
516        !CHECK_BIT(line->bits, IS_CODE)) {
517
518         // IS_QUOTE
519         if(CHECK_BIT(line->bits, IS_QUOTE)) {
520             while(line->text->value[offset] == '>') {
521                 // print a code block
522                 if(colors) {
523                     wattron(window, COLOR_PAIR(CP_CODE));
524                     wprintw(window, "%s", " ");
525                     wattron(window, COLOR_PAIR(CP_FG));
526                     wprintw(window, "%s", " ");
527                 } else {
528                     wprintw(window, "%s", ">");
529                 }
530
531                 // find next quote or break
532                 offset++;
533                 if(line->text->value[offset] == ' ')
534                     offset = next_word(line->text, offset);
535             }
536
537             inline_display(window, &line->text->value[offset], colors);
538         } else {
539
540             // IS_CENTER
541             if(CHECK_BIT(line->bits, IS_CENTER)) {
542                 if(line->length < max_cols) {
543                     wmove(window, y, x + ((max_cols - line->length) / 2));
544                 }
545             }
546
547             // IS_H1 || IS_H2
548             if(CHECK_BIT(line->bits, IS_H1) || CHECK_BIT(line->bits, IS_H2)) {
549
550                 // set headline color
551                 if(colors)
552                     wattron(window, COLOR_PAIR(CP_HEADER));
553
554                 // enable underline for H1
555                 if(CHECK_BIT(line->bits, IS_H1))
556                     wattron(window, A_UNDERLINE);
557
558                 // skip hashes
559                 while(line->text->value[offset] == '#')
560                     offset = next_word(line->text, offset);
561
562                 // print whole lines
563                 waddwstr(window, &line->text->value[offset]);
564
565                 wattroff(window, A_UNDERLINE);
566
567             // no line-wide markdown
568             } else {
569
570                 inline_display(window, &line->text->value[offset], colors);
571             }
572         }
573     }
574
575     // fill rest off line with spaces
576     // we only need this if the color is inverted (e.g. code-blocks)
577     if(CHECK_BIT(line->bits, IS_CODE))
578         for(i = getcurx(window) - x; i < max_cols; i++)
579             wprintw(window, "%s", " ");
580
581     // reset to default color
582     if(colors)
583         wattron(window, COLOR_PAIR(CP_FG));
584     wattroff(window, A_UNDERLINE);
585 }
586
587 void inline_display(WINDOW *window, const wchar_t *c, const int colors) {
588     const static wchar_t *special = L"\\*_`!["; // list of interpreted chars
589     const wchar_t *i = c; // iterator
590     const wchar_t *start_link_name, *start_url;
591     int length_link_name, url_num;
592     cstack_t *stack = cstack_init();
593
594
595     // for each char in line
596     for(; *i; i++) {
597
598         // if char is in special char list
599         if(wcschr(special, *i)) {
600
601             // closing special char (or second backslash)
602             // only if not followed by :alnum:
603             if((stack->top)(stack, *i) &&
604                (!iswalnum(i[1]) || *(i + 1) == L'\0' || *i == L'\\')) {
605
606                 switch(*i) {
607                     // print escaped backslash
608                     case L'\\':
609                         waddnwstr(window, i, 1);
610                         break;
611                     // disable highlight
612                     case L'*':
613                         if(colors)
614                             wattron(window, COLOR_PAIR(CP_FG));
615                         break;
616                     // disable underline
617                     case L'_':
618                         wattroff(window, A_UNDERLINE);
619                         break;
620                     // disable inline code
621                     case L'`':
622                         if(colors)
623                             wattron(window, COLOR_PAIR(CP_FG));
624                         break;
625                 }
626
627                 // remove top special char from stack
628                 (stack->pop)(stack);
629
630             // treat special as regular char
631             } else if((stack->top)(stack, L'\\')) {
632                 waddnwstr(window, i, 1);
633
634                 // remove backslash from stack
635                 (stack->pop)(stack);
636
637             // opening special char
638             } else {
639
640                 // emphasis or code span can start after new-line or space only
641                 // and of cause after another emphasis markup
642                 //TODO this condition looks ugly
643                 if(i == c ||
644                    iswspace(*(i - 1)) ||
645                    ((iswspace(*(i - 1)) || *(i - 1) == L'*' || *(i - 1) == L'_') &&
646                     ((i - 1) == c || iswspace(*(i - 2)))) ||
647                    *i == L'\\') {
648
649                     // url in pandoc style
650                     if ((*i == L'[' && wcschr(i, L']')) ||
651                         (*i == L'!' && *(i + 1) == L'[' && wcschr(i, L']'))) {
652
653                         if (*i == L'!') i++;
654
655                         if (wcschr(i, L']')[1] == L'(' && wcschr(i, L')')) {
656                             i++;
657
658                             // turn higlighting and underlining on
659                             if (colors)
660                                 wattron(window, COLOR_PAIR(CP_HEADER));
661                             wattron(window, A_UNDERLINE);
662
663                             start_link_name = i;
664
665                             // print the content of the label
666                             // the label is printed as is
667                             do {
668                                 waddnwstr(window, i, 1);
669                                 i++;
670                             } while (*i != L']');
671
672                             length_link_name = i - 1 - start_link_name;
673
674                             i++;
675                             i++;
676
677                             start_url = i;
678
679                             while (*i != L')') i++;
680
681                             url_num = url_add(start_link_name, length_link_name, start_url, i - start_url, 0, 0);
682
683                             wprintw(window, " [%d]", url_num);
684
685                             // turn highlighting and underlining off
686                             wattroff(window, A_UNDERLINE);
687                             wattron(window, COLOR_PAIR(CP_FG));
688
689                         } else {
690                             wprintw(window, "[");
691                         }
692
693                     } else switch(*i) {
694                         // enable highlight
695                         case L'*':
696                             if(colors)
697                                 wattron(window, COLOR_PAIR(CP_BOLD));
698                             break;
699                         // enable underline
700                         case L'_':
701                             wattron(window, A_UNDERLINE);
702                             break;
703                         // enable inline code
704                         case L'`':
705                             wattron(window, COLOR_PAIR(CP_CODE));
706                             break;
707                         // do nothing for backslashes
708                     }
709
710                     // push special char to stack
711                     (stack->push)(stack, *i);
712
713                 } else {
714                     waddnwstr(window, i, 1);
715                 }
716             }
717
718         } else {
719             // remove backslash from stack
720             if((stack->top)(stack, L'\\'))
721                 (stack->pop)(stack);
722
723             // print regular char
724             waddnwstr(window, i, 1);
725         }
726     }
727
728     // pop stack until empty to prevent formated trailing spaces
729     while(!(stack->empty)(stack)) {
730         switch((stack->pop)(stack)) {
731             // disable highlight
732             case L'*':
733                 if(colors)
734                     wattron(window, COLOR_PAIR(CP_FG));
735                 break;
736             // disable underline
737             case L'_':
738                 wattroff(window, A_UNDERLINE);
739                 break;
740             // disable inline code
741             case L'`':
742                 if(colors)
743                     wattron(window, COLOR_PAIR(CP_FG));
744                 break;
745             // do nothing for backslashes
746         }
747     }
748
749     (stack->delete)(stack);
750 }
751
752 int int_length (int val) {
753     int l = 1;
754     while(val > 9) {
755         l++;
756         val /= 10;
757     }
758     return l;
759 }
760
761 int get_slide_number(char init) {
762     int retval = init - '0';
763     int c;
764     // block for tenths of a second when using getch, ERR if no input
765     halfdelay(GOTO_SLIDE_DELAY);
766     while((c = getch()) != ERR) {
767         if (c < '0' || c > '9') {
768             retval = -1;
769             break;
770         }
771         retval = (retval * 10) + (c - '0');
772     }
773     nocbreak();     // cancel half delay mode
774     cbreak();       // go back to cbreak
775     return retval;
776 }
777
778 bool evaluate_binding(const int bindings[], char c) {
779     int binding;
780     int ind = 0; 
781     while((binding = bindings[ind]) != 0) {
782         if (c == binding) return true;
783         ind++;
784     }
785     return false;
786 }
787