]> mj.ucw.cz Git - checkmail.git/blob - cm.c
53635efec1e1065e747275c0bec44d56771bbbf4
[checkmail.git] / cm.c
1 /*
2  *      Incoming Mail Checker
3  *
4  *      (c) 2005 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <stdio.h>
8 #include <string.h>
9 #include <stdlib.h>
10 #include <ctype.h>
11 #include <getopt.h>
12 #include <fcntl.h>
13 #include <glob.h>
14 #include <fnmatch.h>
15 #include <sys/stat.h>
16 #include <unistd.h>
17 #include <pwd.h>
18 #include <time.h>
19 #include <curses.h>
20
21 #include "util.h"
22 #include "clists.h"
23
24 static int check_interval = 30;
25 static int force_refresh;
26 static int allow_bells = 1;
27 static int minimum_priority;
28 static time_t last_scan_time;
29 static char *run_cmd = "mutt -f %s";
30
31 struct options {
32   int priority;
33   int hide;
34   int hide_if_empty;
35   int highlight;
36   int beep;
37   int snippets;
38 };
39
40 struct option_node {
41   cnode n;
42   struct options o;
43   char pattern[1];
44 };
45
46 struct pattern_node {
47   cnode n;
48   char *name;
49   char pattern[1];
50 };
51
52 static clist options, patterns;
53 static struct options global_options;
54
55 struct mbox {
56   cnode n;
57   struct options o;
58   char *name;
59   char *path;
60   int index;
61   int scanning;
62   int seen;
63   time_t last_time;
64   int last_size, last_pos;
65   int total, new;
66   int last_total, last_new;
67   int last_beep_new;
68   int force_refresh;
69   char snippet[256];
70 };
71
72 static clist mboxes;
73 static struct mbox **mbox_array;
74 static int num_mboxes, mbox_array_size;
75
76 static void redraw_line(int i);
77 static void rethink_display(void);
78
79 static void
80 add_pattern(char *patt)
81 {
82   struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
83   strcpy(n->pattern, patt);
84   if (patt = strchr(n->pattern, '='))
85     {
86       *patt++ = 0;
87       n->name = patt;
88     }
89   else
90     n->name = NULL;
91   clist_add_tail(&patterns, &n->n);
92 }
93
94 static void
95 add_inbox(void)
96 {
97   struct passwd *p = getpwuid(getuid());
98   if (!p)
99     die("You don't exist, go away!");
100   char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
101   sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
102   add_pattern(buf);
103 }
104
105 static void
106 init_options(struct options *o)
107 {
108   o->priority = -1;
109   o->hide = -1;
110   o->hide_if_empty = -1;
111   o->beep = -1;
112   o->highlight = -1;
113   o->snippets = -1;
114 }
115
116 static void
117 setup_options(struct mbox *b)
118 {
119   b->o = global_options;
120   CLIST_FOR_EACH(struct option_node *, n, options)
121     if (!fnmatch(n->pattern, b->name, 0))
122       {
123         debug("\tApplied options %s\n", n->pattern);
124 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
125         MERGE(priority);
126         MERGE(hide);
127         MERGE(hide_if_empty);
128         MERGE(highlight);
129         MERGE(beep);
130         MERGE(snippets);
131       }
132 }
133
134 static char *
135 mbox_name(char *path)
136 {
137   char *c = strrchr(path, '/');
138   return c ? (c+1) : path;
139 }
140
141 static struct mbox *
142 add_mbox(clist *l, char *path, char *name)
143 {
144   struct mbox *b = xmalloc(sizeof(*b));
145   bzero(b, sizeof(*b));
146   b->path = xstrdup(path);
147   b->name = xstrdup(name);
148
149   if (name)
150     {
151       cnode *prev = l->head.prev;
152       while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
153         prev = prev->prev;
154       clist_insert_after(&b->n, prev);
155     }
156   else
157     clist_add_tail(l, &b->n);
158
159   return b;
160 }
161
162 static void
163 del_mbox(struct mbox *b)
164 {
165   clist_remove(&b->n);
166   free(b->path);
167   free(b->name);
168   free(b);
169 }
170
171 static struct mbox *
172 find_mbox(clist *l, char *path)
173 {
174   CLIST_FOR_EACH(struct mbox *, b, *l)
175     if (!strcmp(b->path, path))
176       return b;
177   return NULL;
178 }
179
180 static inline int
181 mbox_active_p(struct mbox *b)
182 {
183   if (b->o.priority < minimum_priority)
184     return 0;
185   if (b->o.hide)
186     return 0;
187   return 1;
188 }
189
190 static inline int
191 mbox_visible_p(struct mbox *b)
192 {
193   if (!mbox_active_p(b))
194     return 0;
195   if (b->scanning < 0)
196     return 1;
197   if (b->o.hide_if_empty && !b->total)
198     return 0;
199   return 1;
200 }
201
202 static void
203 add_snippet(char **ppos, char *term, char *add)
204 {
205   char *pos = *ppos;
206   while (*add && pos < term)
207     *pos++ = *add++;
208   *ppos = pos;
209   *pos = 0;
210 }
211
212 static void
213 prepare_snippet(struct mbox *b, char *sender, char *subject)
214 {
215   while (*sender == ' ' || *sender == '\t')
216     sender++;
217   while (*subject == ' ' || *subject == '\t')
218     subject++;
219
220   char *pos = b->snippet;
221   char *term = b->snippet + sizeof(b->snippet) - 1;
222   if (sender[0])
223     {
224       add_snippet(&pos, term, sender);
225       add_snippet(&pos, term, ": ");
226     }
227   if (subject[0])
228     add_snippet(&pos, term, subject);
229   else
230     add_snippet(&pos, term, "No subject");
231 }
232
233 static int mb_fd, mb_pos;
234 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
235
236 static void
237 mb_reset(int pos)
238 {
239   mb_cc = mb_end = mb_buf;
240   mb_pos = pos;
241 }
242
243 static int
244 mb_seek(uns pos)
245 {
246   lseek(mb_fd, pos, SEEK_SET);
247   mb_reset(pos);
248 }
249
250 static int
251 mb_tell(void)
252 {
253   return mb_pos - (mb_end - mb_cc);
254 }
255
256 static int
257 mb_ll_get(void)
258 {
259   int len = read(mb_fd, mb_buf, sizeof(mb_buf));
260   mb_cc = mb_buf;
261   if (len <= 0)
262     {
263       mb_end = mb_buf;
264       return -1;
265     }
266   else
267     {
268       mb_end = mb_buf + len;
269       mb_pos += len;
270       return *mb_cc++;
271     }
272 }
273
274 static inline int
275 mb_get(void)
276 {
277   return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
278 }
279
280 static int
281 mb_check(const char *p, int len)
282 {
283   while (len--)
284     {
285       if (mb_get() != *p++)
286         return 0;
287     }
288   return 1;
289 }
290
291 static void
292 scan_mbox(struct mbox *b, struct stat *st)
293 {
294   char buf[1024], sender[1024], subject[1024];
295   int c;
296   const char from[] = "\nFrom ";
297
298   if (!st->st_size)
299     {
300       b->total = b->new = 0;
301       b->last_pos = 0;
302       return;
303     }
304
305   /* FIXME: Locking! */
306
307   mb_fd = open(b->path, O_RDONLY);
308   if (mb_fd < 0)
309     {
310       debug("[open failed: %m] ");
311       b->total = b->new = -1;
312       return;
313     }
314   mb_reset(0);
315
316   int incremental = 0;
317   if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
318     {
319       mb_seek(b->last_pos);
320       if (mb_check(from, 6))
321         {
322           debug("[incremental] ");
323           incremental = 1;
324         }
325       else
326         {
327           debug("[incremental failed] ");
328           mb_seek(0);
329         }
330     }
331   if (!incremental)
332     {
333       if (!mb_check(from+1, 5))
334         {
335           debug("[inconsistent] ");
336           b->total = b->new = -1;
337           goto done;
338         }
339       b->total = b->new = 0;
340       b->last_total = b->last_new = 0;
341     }
342   else
343     {
344       b->total = b->last_total;
345       b->new = b->last_new;
346     }
347
348   for(;;)
349     {
350       b->last_pos = mb_tell() - 5;
351       if (b->last_pos)
352         b->last_pos--;          // last_pos should be the previous \n character
353       b->last_total = b->total;
354       b->last_new = b->new;
355       while ((c = mb_get()) >= 0 && c != '\n')
356         ;
357
358       int new = 1;
359       sender[0] = 0;
360       subject[0] = 0;
361       for (;;)
362         {
363           uns i = 0;
364           for (;;)
365             {
366               c = mb_get();
367               if (c < 0)
368                 {
369                   debug("[truncated] ");
370                   goto done;
371                 }
372               if (c == '\n')
373                 break;
374               if (c == '\r')
375                 continue;
376               if (i < sizeof(buf) - 1)
377                 buf[i++] = c;
378             }
379           buf[i] = 0;
380           if (!buf[0])
381             break;
382           if (!strncasecmp(buf, "Status:", 7))
383             new = 0;
384           else if (!strncasecmp(buf, "From:", 5))
385             strcpy(sender, buf+5);
386           else if (!strncasecmp(buf, "Subject:", 8))
387             strcpy(subject, buf+8);
388         }
389
390       b->total++;
391       if (new)
392         {
393           b->new++;
394           prepare_snippet(b, sender, subject);
395         }
396
397       int ct = 1;
398       while (from[ct])
399         {
400           c = mb_get();
401           if (c < 0)
402             goto done;
403           if (c != from[ct++])
404             ct = (c == '\n');
405         }
406     }
407
408  done:
409   close(mb_fd);
410 }
411
412 static void
413 scan(void)
414 {
415   debug("Searching for mailboxes...\n");
416   last_scan_time = time(NULL);
417   CLIST_FOR_EACH(struct pattern_node *, p, patterns)
418     {
419       debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
420       glob_t g;
421       int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
422       if (err && err != GLOB_NOMATCH)
423         die("Failed to glob %s: %m", p->pattern);
424       for (uns i=0; i<g.gl_pathc; i++)
425         {
426           char *name = g.gl_pathv[i];
427           struct mbox *b = find_mbox(&mboxes, name);
428           if (!b)
429             {
430               b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
431               debug("Discovered mailbox %s (%s)\n", b->name, b->path);
432               setup_options(b);
433               b->scanning = -1;
434             }
435           b->seen = 1;
436         }
437       globfree(&g);
438     }
439
440   struct mbox *tmp;
441   CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
442     {
443       if (b->seen)
444         b->seen = 0;
445       else
446         {
447           debug("Lost mailbox %s\n", b->name);
448           del_mbox(b);
449         }
450     }
451
452   rethink_display();
453
454   debug("Scanning mailboxes...\n");
455   CLIST_FOR_EACH(struct mbox *, b, mboxes)
456     {
457       struct stat st;
458       debug("%s: ", b->name);
459       if (!mbox_active_p(b))
460         {
461           debug("inactive\n");
462           continue;
463         }
464       if (force_refresh)
465         b->force_refresh = 1;
466       if (stat(b->path, &st) < 0)
467         {
468           b->total = b->new = -1;
469           debug("%m\n");
470         }
471       else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
472         {
473           b->scanning = 1;
474           redraw_line(b->index);
475           refresh();
476
477           scan_mbox(b, &st);
478           b->last_time = st.st_mtime;
479           b->last_size = st.st_size;
480           debug("%d %d (stopped at %d of %d)\n", b->total, b->new, b->last_pos, b->last_size);
481
482           b->scanning = 0;
483           redraw_line(b->index);
484           refresh();
485         }
486       else
487         debug("not changed\n");
488       b->force_refresh = 0;
489     }
490   force_refresh = 0;
491
492   debug("Scan finished\n");
493   last_scan_time = time(NULL);
494   rethink_display();
495 }
496
497 static int cursor_at, cursor_max;
498
499 enum {
500   M_IDLE,
501   M_SCAN,
502   M_NEW,
503   M_BAD,
504   M_MAX
505 };
506 static int attrs[2][2][M_MAX];          // active, hilite, status
507
508 static void
509 redraw_line(int i)
510 {
511   move(i, 0);
512   if (i < cursor_max)
513     {
514       struct mbox *b = mbox_array[i];
515       int cc = (cursor_at == i);
516       int hi = b->o.highlight;
517
518       attrset(attrs[cc][hi][M_IDLE]);
519       if (cc)
520         printw("> ");
521       else
522         printw("  ");
523       if (b->new)
524         attrset(attrs[cc][hi][M_NEW]);
525       printw("%-20s ", b->name);
526       if (b->scanning < 0)
527         ;
528       else if (b->scanning)
529         {
530           attrset(attrs[cc][hi][M_SCAN]);
531           printw("[SCANNING]");
532         }
533       else if (b->total < 0)
534         {
535           attrset(attrs[cc][hi][M_BAD]);
536           printw("BROKEN");
537         }
538       else
539         {
540           attrset(attrs[cc][hi][M_IDLE]);
541           printw("%6d ", b->total);
542           if (b->new)
543             {
544               attrset(attrs[cc][hi][M_NEW]);
545               printw("%6d  ", b->new);
546               attrset(attrs[cc][hi][M_IDLE]);
547               int age = (last_scan_time - b->last_time);
548               if (age < 0)
549                 age = 0;
550               if (age < 3600)
551                 printw("%2d min  ", age/60);
552               else if (age < 86400)
553                 printw("%2d hrs  ", age/3600);
554               else
555                 printw("        ");
556               if (b->o.snippets && b->snippet[0])
557                 {
558                   int xx, yy;
559                   getyx(stdscr, yy, xx);
560                   int remains = COLS-1-xx;
561                   if (remains > 2)
562                     printw("%-.*s", remains, b->snippet);
563                 }
564             }
565         }
566     }
567   attrset(attrs[0][0][M_IDLE]);
568   clrtoeol();
569 }
570
571 static void
572 redraw_all(void)
573 {
574   cursor_max = num_mboxes;
575   if (cursor_max > LINES-1)
576     cursor_max = LINES-1;
577   if (cursor_at >= cursor_max)
578     cursor_at = cursor_max - 1;
579   if (cursor_at < 0)
580     cursor_at = 0;
581
582   for (int i=0; i<cursor_max; i++)
583     redraw_line(i);
584   move(cursor_max, 0);
585   if (!cursor_max)
586     {
587       printw("(no mailboxes found)");
588       clrtoeol();
589       move(1, 0);
590     }
591   clrtobot();
592 }
593
594 static void
595 rethink_display(void)
596 {
597   int i = 0;
598   int changed = 0;
599   int beeeep = 0;
600   CLIST_FOR_EACH(struct mbox *, b, mboxes)
601     if (mbox_visible_p(b))
602       {
603         b->index = i;
604         if (i >= num_mboxes || mbox_array[i] != b)
605           {
606             changed = 1;
607             if (i >= mbox_array_size)
608               {
609                 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
610                 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
611               }
612             mbox_array[i] = b;
613           }
614         if (b->o.beep && b->new > b->last_beep_new)
615           beeeep = 1;
616         b->last_beep_new = b->new;
617         i++;
618       }
619   if (i != num_mboxes)
620     changed = 1;
621   num_mboxes = i;
622
623   if (changed)
624     {
625       redraw_all();
626       refresh();
627     }
628   if (beeeep && allow_bells)
629     beep();
630 }
631
632 static void
633 term_init(void)
634 {
635   initscr();
636   cbreak();
637   noecho();
638   nonl();
639   intrflush(stdscr, FALSE);
640   keypad(stdscr, TRUE);
641   curs_set(0);
642
643   static const int attrs_mono[2][M_MAX] = {
644     [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_BAD] = A_DIM },
645     [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_BAD] = A_DIM },
646   };
647   for (int i=0; i<2; i++)
648     for (int j=0; j<M_MAX; j++)
649       {
650         attrs[0][i][j] = attrs_mono[i][j];
651         attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
652       }
653
654   if (has_colors())
655     {
656       start_color();
657       if (COLOR_PAIRS >= 5)
658         {
659           init_pair(1, COLOR_YELLOW, COLOR_BLACK);
660           init_pair(2, COLOR_RED, COLOR_BLACK);
661           init_pair(3, COLOR_WHITE, COLOR_BLUE);
662           init_pair(4, COLOR_YELLOW, COLOR_BLUE);
663           init_pair(5, COLOR_RED, COLOR_BLUE);
664           static const int attrs_color[2][2][M_MAX] = {
665             [0][0] = { [M_IDLE] = 0, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1), [M_BAD] = COLOR_PAIR(2) },
666             [0][1] = { [M_IDLE] = A_BOLD, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1) | A_BOLD, [M_BAD] = COLOR_PAIR(2) | A_BOLD },
667             [1][0] = { [M_IDLE] = COLOR_PAIR(3), [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4), [M_BAD] = COLOR_PAIR(5) },
668             [1][1] = { [M_IDLE] = COLOR_PAIR(3) | A_BOLD, [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4) | A_BOLD, [M_BAD] = COLOR_PAIR(5) | A_BOLD },
669           };
670           memcpy(attrs, attrs_color, sizeof(attrs));
671         }
672     }
673 }
674
675 static void
676 term_cleanup(void)
677 {
678   endwin();
679 }
680
681 static void
682 print_status(char *status)
683 {
684   move(LINES-1, 0);
685   if (status)
686     printw("%s", status);
687   clrtoeol();
688   refresh();
689 }
690
691 static void
692 scan_and_redraw(void)
693 {
694   print_status("Busy...");
695   scan();
696   print_status(NULL);
697 }
698
699 static void
700 move_cursor(int i)
701 {
702   if (i >= 0 && i < cursor_max && i != cursor_at)
703     {
704       int old = cursor_at;
705       cursor_at = i;
706       redraw_line(old);
707       redraw_line(i);
708     }
709 }
710
711 static void
712 next_active(int since, int step)
713 {
714   if (!cursor_max)
715     return;
716   since = (since+cursor_max) % cursor_max;
717   step = (step+cursor_max) % cursor_max;
718   int besti = -1;
719   int bestp = -1;
720   int i = since;
721   do
722     {
723       struct mbox *b = mbox_array[i];
724       if (b->new && b->o.priority > bestp)
725         {
726           besti = i;
727           bestp = b->o.priority;
728         }
729       i = (i+step) % cursor_max;
730     }
731   while (i != since);
732   if (besti >= 0)
733     move_cursor(besti);
734 }
735
736 #define STR2(c) #c
737 #define STR(c) STR2(c)
738
739 static void NONRET
740 usage(void)
741 {
742   fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
743 \n\
744 Options:\n\
745 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
746 -d\t\t\tLog debug messages to stderr\n\
747 -i\t\t\tInclude user's INBOX\n\
748 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
749 -o <pattern>=<opts>\tSet mailbox options\n\
750 -o <opts>\t\tSet default options for all mailboxes\n\
751 -p <pri>\t\tSet minimum priority to show\n\
752 \n\
753 Mailbox options (set with `-o', use upper case to negate):\n\
754 0-9\t\t\tSet mailbox priority (0=default)\n\
755 b\t\t\tBeep when a message arrives\n\
756 e\t\t\tHide from display if empty\n\
757 h\t\t\tHide from display\n\
758 s\t\t\tShow message snippets\n\
759 t\t\t\tHighlight the entry\n\
760 \n\
761 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
762 It can be freely distributed and used according to the GNU GPL v2.\n\
763 ");
764   exit(1);
765 }
766
767 static void
768 parse_options(char *c)
769 {
770   struct options *o;
771   char *sep;
772   if (sep = strchr(c, '='))
773     {
774       struct option_node *n = xmalloc(sizeof(*n) + sep-c);
775       memcpy(n->pattern, c, sep-c);
776       n->pattern[sep-c] = 0;
777       clist_add_tail(&options, &n->n);
778       o = &n->o;
779       init_options(o);
780       c = sep+1;
781     }
782   else
783     o = &global_options;
784
785   int x;
786   while (x = *c++)
787     if (x >= '0' && x <= '9')
788       o->priority = x - '0';
789     else
790       {
791         int value = !!islower(x);
792         switch (tolower(x))
793           {
794           case 'b':
795             o->beep = value;
796             break;
797           case 'e':
798             o->hide_if_empty = value;
799             break;
800           case 'h':
801             o->hide = value;
802             break;
803           case 's':
804             o->snippets = value;
805             break;
806           case 't':
807             o->highlight = value;
808             break;
809           default:
810             fprintf(stderr, "Invalid mailbox option `%c'\n", x);
811             usage();
812           }
813       }
814 }
815
816 int
817 main(int argc, char **argv)
818 {
819   clist_init(&mboxes);
820   clist_init(&options);
821   clist_init(&patterns);
822
823   int c;
824   while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
825     switch (c)
826       {
827       case 'c':
828         check_interval = atol(optarg);
829         if (check_interval <= 0)
830           usage();
831         break;
832       case 'd':
833         debug_mode++;
834         break;
835       case 'i':
836         add_inbox();
837         break;
838       case 'm':
839         run_cmd = optarg;
840         break;
841       case 'o':
842         parse_options(optarg);
843         break;
844       case 'p':
845         minimum_priority = atol(optarg);
846         break;
847       default:
848         usage();
849       }
850   while (optind < argc)
851     add_pattern(argv[optind++]);
852
853   term_init();
854   scan_and_redraw();
855   next_active(0, 1);
856
857   int should_exit = 0;
858   while (!should_exit)
859     {
860       time_t now = time(NULL);
861       int remains = last_scan_time + check_interval - now;
862       if (remains <= 0 || force_refresh)
863         scan_and_redraw();
864       else
865         {
866           remains *= 10;
867           halfdelay((remains > 255) ? 255 : remains);
868           int ch = getch();
869           switch (ch)
870             {
871             case 'q':
872               should_exit = 1;
873               break;
874             case 'j':
875             case KEY_DOWN:
876               move_cursor(cursor_at+1);
877               break;
878             case 'k':
879             case KEY_UP:
880               move_cursor(cursor_at-1);
881               break;
882             case '^':
883             case KEY_HOME:
884             case KEY_PPAGE:
885               move_cursor(0);
886               break;
887             case '$':
888             case KEY_END:
889             case KEY_NPAGE:
890               move_cursor(cursor_max-1);
891               break;
892             case '\t':
893               next_active(cursor_at+1, 1);
894               break;
895             case '`':
896               next_active(cursor_at-1, -1);
897               break;
898             case '\r':
899             case '\n':
900               if (cursor_at < cursor_max)
901                 {
902                   struct mbox *b = mbox_array[cursor_at];
903                   char cmd[strlen(run_cmd) + strlen(b->path) + 16];
904                   sprintf(cmd, run_cmd, b->path);
905                   term_cleanup();
906                   system(cmd);
907                   term_init();
908                   redraw_all();
909                   refresh();
910                   b->force_refresh = 1;
911                   scan_and_redraw();
912                 }
913               break;
914             case 'l' & 0x1f:
915               clearok(stdscr, TRUE);
916               redraw_all();
917               refresh();
918               break;
919             case 'r' & 0x1f:
920               force_refresh = 1;
921               break;
922             case 'b':
923               allow_bells = 1;
924               print_status("Bells and whistles are now enabled. Toot!");
925               break;
926             case 'B':
927               allow_bells = 0;
928               print_status("Bells and whistles are now disabled. Pssst!");
929               break;
930             default:
931               if (ch >= '0' && ch <= '9')
932                 {
933                   minimum_priority = ch - '0';
934                   scan_and_redraw();
935                 }
936               else
937                 debug("Pressed unknown key %d\n", ch);
938             }
939           refresh();
940         }
941     }
942
943   term_cleanup();
944   return 0;
945 }