]> gitweb.fperrin.net Git - iftop.git/blob - ui.c
Import iftop-1.0pre4
[iftop.git] / ui.c
1 /*
2  * ui.c:
3  *
4  */
5
6 #include "config.h"
7
8 #include <sys/types.h>
9
10 #include <ctype.h>
11 #include <ncurses.h>
12 #include <errno.h>
13 #include <string.h>
14 #include <math.h>
15 #include <pthread.h>
16 #include <signal.h>
17 #include <stdlib.h>
18 #include <unistd.h>
19 #include <netdb.h>
20
21 #include <sys/wait.h>
22
23 #include "addr_hash.h"
24 #include "serv_hash.h"
25 #include "iftop.h"
26 #include "resolver.h"
27 #include "sorted_list.h"
28 #include "options.h"
29 #include "screenfilter.h"
30
31 #include "ui_common.h"
32
33 #define HELP_TIME 2
34
35 #define HELP_MESSAGE \
36 "Host display:                          General:\n"\
37 " n - toggle DNS host resolution         P - pause display\n"\
38 " s - toggle show source host            h - toggle this help display\n"\
39 " d - toggle show destination host       b - toggle bar graph display\n"\
40 " t - cycle line display mode            B - cycle bar graph average\n"\
41 "                                        T - toggle cumulative line totals\n"\
42 "Port display:                           j/k - scroll display\n"\
43 " N - toggle service resolution          f - edit filter code\n"\
44 " S - toggle show source port            l - set screen filter\n"\
45 " D - toggle show destination port       L - lin/log scales\n"\
46 " p - toggle port display                ! - shell command\n"\
47 "                                        q - quit\n"\
48 "Sorting:\n"\
49 " 1/2/3 - sort by 1st/2nd/3rd column\n"\
50 " < - sort by source name\n"\
51 " > - sort by dest name\n"\
52 " o - freeze current order\n"\
53 "\n"\
54 "iftop, version " PACKAGE_VERSION
55
56
57 extern hash_type* history;
58 extern int history_pos;
59 extern int history_len;
60
61 extern options_t options ;
62
63 void ui_finish();
64
65 #define HELP_MSG_SIZE 80
66 int showhelphint = 0;
67 int persistenthelp = 0;
68 time_t helptimer = 0;
69 char helpmsg[HELP_MSG_SIZE];
70 int dontshowdisplay = 0;
71
72 /* Barchart scales. */
73 static struct {
74     int max, interval;
75 } scale[] = {
76         {      64000,     10 },     /* 64 kbit/s */
77         {     128000,     10 },
78         {     256000,     10 },
79         {    1000000,     10 },     /* 1 Mbit/s */
80         {   10000000,     10 },     
81         {  100000000,    100 },
82         { 1000000000,    100 }      /* 1 Gbit/s */
83     };
84 static int rateidx = 0, wantbiggerrate;
85
86 static int rateidx_init = 0;
87
88 static int get_bar_interval(float bandwidth) {
89     int i = 10;
90     if(bandwidth > 100000000) {
91         i = 100;
92     }
93     return i;
94 }
95
96 static float get_max_bandwidth() {
97     float max;
98     if(options.max_bandwidth > 0) {
99         max = options.max_bandwidth;
100     }
101     else {
102         max = scale[rateidx].max;
103     }
104     return max;
105 }
106
107 /* rate in bits */
108 static int get_bar_length(const int rate) {
109     float l;
110     if (rate <= 0)
111         return 0;
112     if (rate > scale[rateidx].max) {
113       wantbiggerrate = 1;
114       if(! rateidx_init) {
115         while(rate > scale[rateidx_init++].max) {
116         }
117         rateidx = rateidx_init;
118       }
119     }
120     if(options.log_scale) {
121         l = log(rate) / log(get_max_bandwidth());
122     }
123     else {
124         l = rate / get_max_bandwidth();
125     }
126     return (l * COLS);
127 }
128
129 static void draw_bar_scale(int* y) {
130     float i;
131     float max,interval;
132     max = get_max_bandwidth();
133     interval = get_bar_interval(max);
134     if(options.showbars) {
135         float stop;
136         /* Draw bar graph scale on top of the window. */
137         move(*y, 0);
138         clrtoeol();
139         mvhline(*y + 1, 0, 0, COLS);
140         /* i in bytes */
141
142         if(options.log_scale) {
143             i = 1.25;
144             stop = max / 8;
145         }
146         else {
147             i = max / (5 * 8);
148             stop = max / 8;
149         }
150
151         /* for (i = 1.25; i * 8 <= max; i *= interval) { */
152         while(i <= stop) {
153             char s[40], *p;
154             int x;
155             /* This 1024 vs 1000 stuff is just plain evil */
156             readable_size(i, s, sizeof s, options.log_scale ? 1000 : 1024, options.bandwidth_in_bytes);
157             p = s + strspn(s, " ");
158             x = get_bar_length(i * 8);
159             mvaddch(*y + 1, x, ACS_BTEE);
160             if (x + strlen(p) >= COLS)
161                 x = COLS - strlen(p);
162             mvaddstr(*y, x, p);
163
164             if(options.log_scale) {
165                 i *= interval;
166             }
167             else {
168                 i += max / (5 * 8);
169             }
170         }
171         mvaddch(*y + 1, 0, ACS_LLCORNER);
172         *y += 2;
173     }
174     else {
175         mvhline(*y, 0, 0, COLS);
176         *y += 1;
177     }
178 }
179
180 void draw_line_total(float sent, float recv, int y, int x, option_linedisplay_t linedisplay, int bytes) {
181     char buf[10];
182     float n = 0;
183     switch(linedisplay) {
184         case OPTION_LINEDISPLAY_TWO_LINE:
185           draw_line_total(sent, recv, y, x, OPTION_LINEDISPLAY_ONE_LINE_SENT, bytes);
186           draw_line_total(sent, recv, y+1, x, OPTION_LINEDISPLAY_ONE_LINE_RECV, bytes);
187           break;
188         case OPTION_LINEDISPLAY_ONE_LINE_SENT:
189           n = sent;
190           break;
191         case OPTION_LINEDISPLAY_ONE_LINE_RECV:
192           n = recv;
193           break;
194         case OPTION_LINEDISPLAY_ONE_LINE_BOTH:
195           n = recv + sent;
196           break;
197     }
198     if(linedisplay != OPTION_LINEDISPLAY_TWO_LINE) {
199         readable_size(n, buf, 10, 1024, bytes);
200         mvaddstr(y, x, buf);
201     }
202 }
203
204 void draw_bar(float n, int y) {
205     int L;
206     mvchgat(y, 0, -1, A_NORMAL, 0, NULL);
207     L = get_bar_length(8 * n);
208     if (L > 0)
209         mvchgat(y, 0, L + 1, A_REVERSE, 0, NULL);
210 }
211
212 void draw_line_totals(int y, host_pair_line* line, option_linedisplay_t linedisplay) {
213     int j;
214     int x = (COLS - 8 * HISTORY_DIVISIONS);
215
216     for(j = 0; j < HISTORY_DIVISIONS; j++) {
217         draw_line_total(line->sent[j], line->recv[j], y, x, linedisplay, options.bandwidth_in_bytes);
218         x += 8;
219     }
220     
221     if(options.showbars) {
222       switch(linedisplay) {
223         case OPTION_LINEDISPLAY_TWO_LINE:
224           draw_bar(line->sent[options.bar_interval],y);
225           draw_bar(line->recv[options.bar_interval],y+1);
226           break;
227         case OPTION_LINEDISPLAY_ONE_LINE_SENT:
228           draw_bar(line->sent[options.bar_interval],y);
229           break;
230         case OPTION_LINEDISPLAY_ONE_LINE_RECV:
231           draw_bar(line->recv[options.bar_interval],y);
232           break;
233         case OPTION_LINEDISPLAY_ONE_LINE_BOTH:
234           draw_bar(line->recv[options.bar_interval] + line->sent[options.bar_interval],y);
235           break;
236       }
237     }
238 }
239
240 void draw_totals(host_pair_line* totals) {
241     /* Draw rule */
242     int y = LINES - 4;
243     int j;
244     char buf[10];
245     int x = (COLS - 8 * HISTORY_DIVISIONS);
246     y++;
247     draw_line_totals(y, totals, OPTION_LINEDISPLAY_TWO_LINE);
248     y += 2;
249     for(j = 0; j < HISTORY_DIVISIONS; j++) {
250         readable_size((totals->sent[j] + totals->recv[j]) , buf, 10, 1024, options.bandwidth_in_bytes);
251         mvaddstr(y, x, buf);
252         x += 8;
253     }
254 }
255
256 extern history_type history_totals;
257
258
259 void ui_print() {
260     sorted_list_node* nn = NULL;
261     char host1[HOSTNAME_LENGTH], host2[HOSTNAME_LENGTH];
262     static char *line;
263     static int lcols;
264     int y = 0;
265
266     if (dontshowdisplay)
267         return;
268
269     if (!line || lcols != COLS) {
270         xfree(line);
271         line = calloc(COLS + 1, 1);
272     }
273
274     /* 
275      * erase() is faster than clear().  Dunno why we switched to 
276      * clear() -pdw 24/10/02
277      */
278     erase();
279
280     draw_bar_scale(&y);
281
282     if(options.showhelp) {
283       mvaddstr(y,0,HELP_MESSAGE);
284     }
285     else {
286       int i = 0;
287
288       while(i < options.screen_offset && ((nn = sorted_list_next_item(&screen_list, nn)) != NULL)) {
289         i++;
290       }
291
292       /* Screen layout: we have 2 * HISTORY_DIVISIONS 6-character wide history
293        * items, and so can use COLS - 12 * HISTORY_DIVISIONS to print the two
294        * host names. */
295
296       if(i == 0 || nn != NULL) {
297         while((y < LINES - 5) && ((nn = sorted_list_next_item(&screen_list, nn)) != NULL)) {
298             int x = 0, L;
299
300
301             host_pair_line* screen_line = (host_pair_line*)nn->data;
302
303             if(y < LINES - 5) {
304                 L = (COLS - 8 * HISTORY_DIVISIONS - 4) / 2;
305                 if(options.show_totals) {
306                     L -= 4;    
307                 }
308                 if(L > HOSTNAME_LENGTH) {
309                     L = HOSTNAME_LENGTH;
310                 }
311
312                 sprint_host(host1, screen_line->ap.af,
313                             &(screen_line->ap.src6),
314                             screen_line->ap.src_port,
315                             screen_line->ap.protocol, L, options.aggregate_src);
316                 sprint_host(host2, screen_line->ap.af,
317                             &(screen_line->ap.dst6),
318                             screen_line->ap.dst_port,
319                             screen_line->ap.protocol, L, options.aggregate_dest);
320
321                 if(!screen_filter_match(host1) && !screen_filter_match(host2)) {
322                   continue;
323                 }
324
325                 mvaddstr(y, x, host1);
326                 x += L;
327
328                 switch(options.linedisplay) {
329                   case OPTION_LINEDISPLAY_TWO_LINE:
330                     mvaddstr(y, x, " => ");
331                     mvaddstr(y+1, x, " <= ");
332                     break;
333                   case OPTION_LINEDISPLAY_ONE_LINE_BOTH:
334                     mvaddstr(y, x, "<=> ");
335                     break;
336                   case OPTION_LINEDISPLAY_ONE_LINE_SENT:
337                     mvaddstr(y, x, " => ");
338                     break;
339                   case OPTION_LINEDISPLAY_ONE_LINE_RECV:
340                     mvaddstr(y, x, " <= ");
341                     break;
342                 }
343
344                 x += 4;
345
346
347                 mvaddstr(y, x, host2);
348                 
349                 if(options.show_totals) {
350                     draw_line_total(screen_line->total_sent, screen_line->total_recv, y, COLS - 8 * (HISTORY_DIVISIONS + 1), options.linedisplay, 1);
351                 }
352
353                 draw_line_totals(y, screen_line, options.linedisplay);
354
355             }
356             if(options.linedisplay == OPTION_LINEDISPLAY_TWO_LINE) {
357               y += 2;
358             }
359             else {
360               y += 1;
361             }
362         }
363       }
364     }
365
366
367     y = LINES - 3;
368     
369     mvhline(y-1, 0, 0, COLS);
370
371     mvaddstr(y, 0, "TX: ");
372     mvaddstr(y+1, 0, "RX: ");
373     mvaddstr(y+2, 0, "TOTAL: ");
374
375     /* Cummulative totals */
376     mvaddstr(y, 16, "cum: ");
377
378     readable_size(history_totals.total_sent, line, 10, 1024, 1);
379     mvaddstr(y, 22, line);
380
381     readable_size(history_totals.total_recv, line, 10, 1024, 1);
382     mvaddstr(y+1, 22, line);
383
384     readable_size(history_totals.total_recv + history_totals.total_sent, line, 10, 1024, 1);
385     mvaddstr(y+2, 22, line);
386
387     /* peak traffic */
388     mvaddstr(y, 32, "peak: ");
389
390     readable_size(peaksent / RESOLUTION, line, 10, 1024, options.bandwidth_in_bytes);
391     mvaddstr(y, 39, line);
392
393     readable_size(peakrecv / RESOLUTION, line, 10, 1024, options.bandwidth_in_bytes);
394     mvaddstr(y+1, 39, line);
395
396     readable_size(peaktotal / RESOLUTION, line, 10, 1024, options.bandwidth_in_bytes);
397     mvaddstr(y+2, 39, line);
398
399     mvaddstr(y, COLS - 8 * HISTORY_DIVISIONS - 8, "rates:");
400
401     draw_totals(&totals);
402
403
404     if(showhelphint) {
405       mvaddstr(0, 0, " ");
406       mvaddstr(0, 1, helpmsg);
407       mvaddstr(0, 1 + strlen(helpmsg), " ");
408       mvchgat(0, 0, strlen(helpmsg) + 2, A_REVERSE, 0, NULL);
409     }
410     move(LINES - 1, COLS - 1);
411     
412     refresh();
413
414     /* Bar chart auto scale */
415     if (wantbiggerrate && options.max_bandwidth == 0) {
416       ++rateidx;
417       wantbiggerrate = 0;
418     }
419 }
420
421 void ui_tick(int print) {
422   if(print) {
423     ui_print();
424   }
425   else if(showhelphint && (time(NULL) - helptimer > HELP_TIME) && !persistenthelp) {
426     showhelphint = 0;
427     ui_print();
428   }
429 }
430
431 void ui_curses_init() {
432     (void) initscr();      /* initialize the curses library */
433     keypad(stdscr, TRUE);  /* enable keyboard mapping */
434     (void) nonl();         /* tell curses not to do NL->CR/NL on output */
435     (void) cbreak();       /* take input chars one at a time, no wait for \n */
436     (void) noecho();       /* don't echo input */
437     (void) curs_set(0);    /* hide blinking cursor in ui */
438     halfdelay(2);
439 }
440
441 void showhelp(const char * s) {
442   strncpy(helpmsg, s, HELP_MSG_SIZE);
443   showhelphint = 1;
444   helptimer = time(NULL);
445   persistenthelp = 0;
446   tick(1);
447 }
448
449 void ui_init() {
450     char msg[20];
451     ui_curses_init();
452     
453     erase();
454
455     screen_list_init();
456     screen_hash = addr_hash_create();
457
458     service_hash = serv_hash_create();
459     serv_hash_initialise(service_hash);
460
461     snprintf(msg,20,"Listening on %s",options.interface);
462     showhelp(msg);
463
464
465 }
466
467
468 void showportstatus() {
469   if(options.showports == OPTION_PORTS_ON) {
470     showhelp("Port display ON");
471   }
472   else if(options.showports == OPTION_PORTS_OFF) {
473     showhelp("Port display OFF");
474   }
475   else if(options.showports == OPTION_PORTS_DEST) {
476     showhelp("Port display DEST");
477   }
478   else if(options.showports == OPTION_PORTS_SRC) {
479     showhelp("Port display SOURCE");
480   }
481 }
482
483
484 void ui_loop() {
485     /* in edline.c */
486     char *edline(int linenum, const char *prompt, const char *initial);
487     /* in iftop.c */
488     char *set_filter_code(const char *filter);
489
490     extern sig_atomic_t foad;
491
492     while(foad == 0) {
493         int i;
494         i = getch();
495         switch (i) {
496             case 'q':
497                 foad = 1;
498                 break;
499
500             case 'n':
501                 if(options.dnsresolution) {
502                     options.dnsresolution = 0;
503                     showhelp("DNS resolution off");
504                 }
505                 else {
506                     options.dnsresolution = 1;
507                     showhelp("DNS resolution on");
508                 }
509                 tick(1);
510                 break;
511
512             case 'N':
513                 if(options.portresolution) {
514                     options.portresolution = 0;
515                     showhelp("Port resolution off");
516                 }
517                 else {
518                     options.portresolution = 1;
519                     showhelp("Port resolution on");
520                 }
521                 tick(1);
522                 break;
523
524             case 'h':
525             case '?':
526                 options.showhelp = !options.showhelp;
527                 tick(1);
528                 break;
529
530             case 'b':
531                 if(options.showbars) {
532                     options.showbars = 0;
533                     showhelp("Bars off");
534                 }
535                 else {
536                     options.showbars = 1;
537                     showhelp("Bars on");
538                 }
539                 tick(1);
540                 break;
541
542             case 'B':
543                 options.bar_interval = (options.bar_interval + 1) % 3;
544                 if(options.bar_interval == 0) {
545                     showhelp("Bars show 2s average");
546                 }
547                 else if(options.bar_interval == 1) { 
548                     showhelp("Bars show 10s average");
549                 }
550                 else {
551                     showhelp("Bars show 40s average");
552                 }
553                 ui_print();
554                 break;
555             case 's':
556                 if(options.aggregate_src) {
557                     options.aggregate_src = 0;
558                     showhelp("Show source host");
559                 }
560                 else {
561                     options.aggregate_src = 1;
562                     showhelp("Hide source host");
563                 }
564                 break;
565             case 'd':
566                 if(options.aggregate_dest) {
567                     options.aggregate_dest = 0;
568                     showhelp("Show dest host");
569                 }
570                 else {
571                     options.aggregate_dest = 1;
572                     showhelp("Hide dest host");
573                 }
574                 break;
575             case 'S':
576                 /* Show source ports */
577                 if(options.showports == OPTION_PORTS_OFF) {
578                   options.showports = OPTION_PORTS_SRC;
579                 }
580                 else if(options.showports == OPTION_PORTS_DEST) {
581                   options.showports = OPTION_PORTS_ON;
582                 }
583                 else if(options.showports == OPTION_PORTS_ON) {
584                   options.showports = OPTION_PORTS_DEST;
585                 }
586                 else {
587                   options.showports = OPTION_PORTS_OFF;
588                 }
589                 showportstatus();
590                 break;
591             case 'D':
592                 /* Show dest ports */
593                 if(options.showports == OPTION_PORTS_OFF) {
594                   options.showports = OPTION_PORTS_DEST;
595                 }
596                 else if(options.showports == OPTION_PORTS_SRC) {
597                   options.showports = OPTION_PORTS_ON;
598                 }
599                 else if(options.showports == OPTION_PORTS_ON) {
600                   options.showports = OPTION_PORTS_SRC;
601                 }
602                 else {
603                   options.showports = OPTION_PORTS_OFF;
604                 }
605                 showportstatus();
606                 break;
607             case 'p':
608                 options.showports = 
609                   (options.showports == OPTION_PORTS_OFF)
610                   ? OPTION_PORTS_ON
611                   : OPTION_PORTS_OFF;
612                 showportstatus();
613                 // Don't tick here, otherwise we get a bogus display
614                 break;
615             case 'P':
616                 if(options.paused) {
617                     options.paused = 0;
618                     showhelp("Display unpaused");
619                 }
620                 else {
621                     options.paused = 1;
622                     showhelp("Display paused");
623                     persistenthelp = 1;
624                 }
625                 break;
626             case 'o':
627                 if(options.freezeorder) {
628                     options.freezeorder = 0;
629                     showhelp("Order unfrozen");
630                 }
631                 else {
632                     options.freezeorder = 1;
633                     showhelp("Order frozen");
634                     persistenthelp = 1;
635                 }
636                 break;
637             case '1':
638                 options.sort = OPTION_SORT_DIV1;
639                 showhelp("Sort by col 1");
640                 break;
641             case '2':
642                 options.sort = OPTION_SORT_DIV2;
643                 showhelp("Sort by col 2");
644                 break;
645             case '3':
646                 options.sort = OPTION_SORT_DIV3;
647                 showhelp("Sort by col 3");
648                 break;
649             case '<':
650                 options.sort = OPTION_SORT_SRC;
651                 showhelp("Sort by source");
652                 break;
653             case '>':
654                 options.sort = OPTION_SORT_DEST;
655                 showhelp("Sort by dest");
656                 break;
657             case 'j':
658                 options.screen_offset++;
659                 ui_print();
660                 break;
661             case 'k':
662                 if(options.screen_offset > 0) {
663                   options.screen_offset--;
664                   ui_print();
665                 }
666                 break;
667             case 't':
668                 options.linedisplay = (options.linedisplay + 1) % 4;
669                 switch(options.linedisplay) {
670                   case OPTION_LINEDISPLAY_TWO_LINE:
671                     showhelp("Two lines per host");
672                     break;
673                   case OPTION_LINEDISPLAY_ONE_LINE_SENT:
674                     showhelp("Sent traffic only");
675                     break;
676                   case OPTION_LINEDISPLAY_ONE_LINE_RECV:
677                     showhelp("Received traffic only");
678                     break;
679                   case OPTION_LINEDISPLAY_ONE_LINE_BOTH:
680                     showhelp("One line per host");
681                     break;
682                 }
683                 ui_print();
684                 break;
685             case 'f': {
686                 char *s;
687                 dontshowdisplay = 1;
688                 if ((s = edline(0, "Net filter", options.filtercode))) {
689                     char *m;
690                     if (s[strspn(s, " \t")] == 0) {
691                         /* Empty filter; set to NULL. */
692                         xfree(s);
693                         s = NULL;
694                     }
695                     if (!(m = set_filter_code(s))) {
696                         xfree(options.filtercode);
697                         options.filtercode = s;
698                         /* -lpcap will write junk to stderr; we do our best to
699                          * erase it.... */
700                         move(COLS - 1, LINES - 1);
701                         wrefresh(curscr);
702                         showhelp("Installed new filter");
703                     } else {
704                         showhelp(m);
705                         xfree(s);
706                     }
707                 }
708                 dontshowdisplay = 0;
709                 ui_print();
710                 break;
711             }
712             case 'l': {
713 #ifdef HAVE_REGCOMP
714                 char *s;
715                 dontshowdisplay = 1;
716                 if ((s = edline(0, "Screen filter", options.screenfilter))) {
717                     if(!screen_filter_set(s)) {
718                         showhelp("Invalid regexp");
719                     }
720                 }
721                 dontshowdisplay = 0;
722                 ui_print();
723 #else
724                 showhelp("Sorry, screen filters not supported on this platform")
725 #endif
726                 break;
727             }
728             case '!': {
729 #ifdef ALLOW_SUBSHELL
730                 char *s;
731                 dontshowdisplay = 1;
732                 if ((s = edline(0, "Command", "")) && s[strspn(s, " \t")]) {
733                     int i, dowait = 0;
734                     erase();
735                     refresh();
736                     endwin();
737                     errno = 0;
738                     i = system(s);
739                     if (i == -1 || (i == 127 && errno != 0)) {
740                         fprintf(stderr, "system: %s: %s\n", s, strerror(errno));
741                         dowait = 1;
742                     } else if (i != 0) {
743                         if (WIFEXITED(i))
744                             fprintf(stderr, "%s: exited with code %d\n", s, WEXITSTATUS(i));
745                         else if (WIFSIGNALED(i))
746                             fprintf(stderr, "%s: killed by signal %d\n", s, WTERMSIG(i));
747                         dowait = 1;
748                     }
749                     ui_curses_init();
750                     if (dowait) {
751                         fprintf(stderr, "Press any key....");
752                         while (getch() == ERR);
753                     }
754                     erase();
755                     xfree(s);
756                 }
757                 dontshowdisplay = 0;
758 #else
759                 showhelp("Sorry, subshells have been disabled.");
760 #endif
761                 break;
762             }
763             case 'T':
764                 options.show_totals = !options.show_totals;
765                 if(options.show_totals) {
766                     showhelp("Show cumulative totals");
767                 }
768                 else {
769                     showhelp("Hide cumulative totals");
770                 }
771                 ui_print();
772                 break;
773             case 'L':
774                 options.log_scale = !options.log_scale;
775                 showhelp(options.log_scale ? "Logarithmic scale" : "Linear scale");
776                 ui_print();
777                 break;
778             case KEY_CLEAR:
779             case 12:    /* ^L */
780                 wrefresh(curscr);
781                 break;
782             case ERR:
783                 break;
784             default:
785                 showhelp("Press H or ? for help");
786                 break;
787         }
788         tick(0);
789     }
790 }
791
792 void ui_finish() {
793     endwin();
794 }