/*
* Incoming Mail Checker
*
- * (c) 2005 Martin Mares <mj@ucw.cz>
+ * (c) 2005--2015 Martin Mares <mj@ucw.cz>
*/
-#include <stdio.h>
-#include <string.h>
-#include <stdlib.h>
+#define _GNU_SOURCE
+
#include <ctype.h>
-#include <getopt.h>
+#include <dirent.h>
#include <fcntl.h>
-#include <glob.h>
#include <fnmatch.h>
-#include <sys/stat.h>
-#include <unistd.h>
+#include <getopt.h>
+#include <glob.h>
#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
#include <time.h>
+#include <unistd.h>
+
+#ifdef CONFIG_WIDE_CURSES
+#include <ncursesw/ncurses.h>
+#else
#include <curses.h>
+#endif
#include "util.h"
#include "clists.h"
+#include "charset.h"
static int check_interval = 30;
static int force_refresh;
static int allow_bells = 1;
+static int allow_osd = 1;
static int minimum_priority;
static time_t last_scan_time;
static char *run_cmd = "mutt -f %s";
+static int simple_tab;
struct options {
int priority;
int highlight;
int beep;
int snippets;
+ int show_flagged;
+ int sender_personal;
+ int sender_mbox;
+ int hotkey;
+ int led;
+ int osd;
+ int unread_is_new;
};
struct option_node {
};
static clist options, patterns;
-static struct options global_options;
+static struct options global_options = {
+ .sender_personal = 1
+};
+
+#define MDIR_MAX_NAME_LEN 128
struct mbox {
cnode n;
int scanning;
int seen;
time_t last_time;
+ time_t best_time;
+ time_t display_valid_until;
int last_size, last_pos;
- int total, new;
- int last_total, last_new;
+ int total, new, flagged;
+ int last_total, last_new, last_flagged;
int last_beep_new;
int force_refresh;
- char snippet[256];
+ int best_is_new;
+ char sender_snippet[128];
+ char subject_snippet[128];
+ char mdir_best[MDIR_MAX_NAME_LEN];
};
static clist mboxes;
static struct mbox **mbox_array;
static int num_mboxes, mbox_array_size;
+struct osd_opt_node {
+ cnode n;
+ char *val;
+ char key[1];
+};
+
+static clist osd_opts;
+
static void redraw_line(int i);
-static void rethink_display(void);
+static void rethink_display(int notify);
static void
add_pattern(char *patt)
o->beep = -1;
o->highlight = -1;
o->snippets = -1;
+ o->show_flagged = -1;
+ o->sender_personal = -1;
+ o->sender_mbox = -1;
+ o->hotkey = -1;
+ o->led = -1;
+ o->osd = -1;
+ o->unread_is_new = -1;
}
static void
MERGE(highlight);
MERGE(beep);
MERGE(snippets);
+ MERGE(show_flagged);
+ MERGE(sender_personal);
+ MERGE(sender_mbox);
+ MERGE(hotkey);
+ MERGE(led);
+ MERGE(osd);
+ MERGE(unread_is_new);
}
}
+static void
+add_osd_opt(char *arg)
+{
+ struct osd_opt_node *n = xmalloc(sizeof(*n) + strlen(arg));
+ strcpy(n->key, arg);
+ n->val = strchr(n->key, '=');
+ if (!n->val)
+ die("Malformed OSD option");
+ *n->val++ = 0;
+ clist_add_tail(&osd_opts, &n->n);
+}
+
static char *
mbox_name(char *path)
{
}
static void
-add_snippet(char **ppos, char *term, char *add)
+prepare_snippets(struct mbox *b, char *sender, char *subject)
{
- char *pos = *ppos;
- while (*add && pos < term)
- *pos++ = *add++;
- *ppos = pos;
- *pos = 0;
-}
+ char *pos, *term;
-static void
-prepare_snippet(struct mbox *b, char *sender, char *subject)
-{
while (*sender == ' ' || *sender == '\t')
sender++;
while (*subject == ' ' || *subject == '\t')
subject++;
- char *pos = b->snippet;
- char *term = b->snippet + sizeof(b->snippet) - 1;
- if (sender[0])
- {
- add_snippet(&pos, term, sender);
- add_snippet(&pos, term, ": ");
- }
+ pos = b->sender_snippet;
+ term = pos + sizeof(b->sender_snippet) - 1;
+ if (sender[0] && (b->o.sender_mbox || b->o.sender_personal))
+ add_addr_snippet(&pos, term, sender, b->o.sender_mbox, b->o.sender_personal);
+ else
+ *pos = 0;
+
+ pos = b->subject_snippet;
+ term = pos + sizeof(b->subject_snippet) - 1;
if (subject[0])
- add_snippet(&pos, term, subject);
+ add_subject_snippet(&pos, term, subject);
else
- add_snippet(&pos, term, "No subject");
+ add_snippet_raw(&pos, term, "No subject");
}
-static int mb_fd, mb_pos;
+static void
+build_snippet(char *buf, char *term, struct mbox *b)
+{
+ if (b->sender_snippet[0])
+ {
+ add_snippet(&buf, term, b->sender_snippet);
+ add_snippet_raw(&buf, term, ": ");
+ }
+ add_snippet(&buf, term, b->subject_snippet);
+}
+
+static int mb_fd, mb_pos, mb_seekable;
static unsigned char mb_buf[4096], *mb_cc, *mb_end;
static void
mb_pos = pos;
}
-static int
+static void
mb_seek(uns pos)
{
lseek(mb_fd, pos, SEEK_SET);
return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
}
+static void
+mb_unget(int c)
+{
+ if (c >= 0)
+ mb_cc--;
+}
+
static int
mb_check(const char *p, int len)
{
return 1;
}
+static void
+mb_skip(int len)
+{
+ while (len)
+ {
+ int avail = mb_end - mb_cc;
+ if (!avail)
+ {
+ if (mb_seekable && len >= (int) sizeof(mb_buf))
+ {
+ int next = len - len % sizeof(mb_buf);
+ mb_seek(mb_tell() + next);
+ len -= next;
+ continue;
+ }
+ if (mb_ll_get() < 0)
+ return;
+ len--;
+ }
+ else
+ {
+ int next = (avail < len) ? avail : len;
+ len -= next;
+ mb_cc += next;
+ }
+ }
+}
+
+#define HDR_BUF_SIZE 1024
+
+static int
+read_hdr_field(char *buf)
+{
+ uns i = 0;
+ for (;;)
+ {
+ int c = mb_get();
+ if (c < 0)
+ {
+ debug("[truncated] ");
+ return -1;
+ }
+ if (c == '\n')
+ {
+ if (!i)
+ break;
+ int fold = -1;
+ do
+ {
+ fold++;
+ c = mb_get();
+ }
+ while (c == ' ' || c == '\t');
+ mb_unget(c);
+ if (!fold)
+ break;
+ c = ' ';
+ }
+ if (c == '\r')
+ continue;
+ if (i < HDR_BUF_SIZE - 1)
+ buf[i++] = c;
+ }
+ buf[i] = 0;
+ if (debug_mode > 2)
+ debug("Header: <%s>\n", buf);
+ return buf[0] ? 1 : 0;
+}
+
static void
scan_mbox(struct mbox *b, struct stat *st)
{
- char buf[1024], sender[1024], subject[1024];
+ char buf[HDR_BUF_SIZE], sender[HDR_BUF_SIZE], subject[HDR_BUF_SIZE];
int c;
+ int compressed = 0;
const char from[] = "\nFrom ";
+ b->best_time = st->st_mtime;
if (!st->st_size)
{
- b->total = b->new = 0;
+ b->total = b->new = b->flagged = 0;
b->last_pos = 0;
return;
}
- /* FIXME: Locking! */
-
mb_fd = open(b->path, O_RDONLY);
if (mb_fd < 0)
{
debug("[open failed: %m] ");
- b->total = b->new = -1;
+ b->total = b->new = b->flagged = -1;
return;
}
+ mb_seekable = 1;
+
+ char signature[2];
+ c = read(mb_fd, signature, 2);
+ lseek(mb_fd, 0, SEEK_SET);
+
+ if (c == 2 && !memcmp(signature, "\037\213", 2)) //gzip
+ {
+ debug("[decompressing] ");
+ int fds[2];
+ if (pipe(fds))
+ die("pipe failed: %m");
+ int pid = fork();
+ if (pid < 0)
+ die("fork failed: %m");
+ if (!pid)
+ {
+ if (dup2(mb_fd, 0) < 0 || dup2(fds[1], 1) < 0)
+ die("dup2 failed: %m");
+ close(fds[0]);
+ close(fds[1]);
+ close(mb_fd);
+ execlp("gzip", "gzip", "-cd", NULL);
+ die("Cannot execute gzip: %m");
+ }
+ close(fds[1]);
+ close(mb_fd);
+ mb_fd = fds[0];
+ compressed = 1;
+ mb_seekable = 0;
+ }
mb_reset(0);
int incremental = 0;
- if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
+ if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh && mb_seekable)
{
mb_seek(b->last_pos);
if (mb_check(from, 6))
if (!mb_check(from+1, 5))
{
debug("[inconsistent] ");
- b->total = b->new = -1;
+ b->total = b->new = b->flagged = -1;
goto done;
}
- b->total = b->new = 0;
- b->last_total = b->last_new = 0;
+ b->total = b->new = b->flagged = 0;
+ b->last_total = b->last_new = b->last_flagged = 0;
+ b->best_is_new = 0;
}
else
{
b->total = b->last_total;
b->new = b->last_new;
+ b->flagged = b->last_flagged;
}
for(;;)
b->last_pos--; // last_pos should be the previous \n character
b->last_total = b->total;
b->last_new = b->new;
+ b->last_flagged = b->flagged;
while ((c = mb_get()) >= 0 && c != '\n')
;
+ int content_length = -1;
int new = 1;
+ int flagged = 0;
sender[0] = 0;
subject[0] = 0;
for (;;)
{
- uns i = 0;
- for (;;)
- {
- c = mb_get();
- if (c < 0)
- {
- debug("[truncated] ");
- goto done;
- }
- if (c == '\n')
- break;
- if (c == '\r')
- continue;
- if (i < sizeof(buf) - 1)
- buf[i++] = c;
- }
- buf[i] = 0;
- if (!buf[0])
+ int c = read_hdr_field(buf);
+ if (c < 0)
+ goto done;
+ if (!c)
break;
if (!strncasecmp(buf, "Status:", 7))
- new = 0;
+ {
+ if (!b->o.unread_is_new || strchr(buf + 7, 'R'))
+ new = 0;
+ }
+ else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
+ flagged = 1;
else if (!strncasecmp(buf, "From:", 5))
strcpy(sender, buf+5);
else if (!strncasecmp(buf, "Subject:", 8))
strcpy(subject, buf+8);
+ else if (!strncasecmp(buf, "Content-Length:", 15))
+ content_length = atoi(buf + 15);
}
b->total++;
if (new)
+ b->new++;
+ if (flagged)
+ b->flagged++;
+ if (debug_mode > 1)
+ debug("new=%d flagged=%d len=%d sender=<%s> subject=<%s>\n", new, flagged, content_length, sender, subject);
+ if (new || (flagged && !b->best_is_new))
{
- b->new++;
- prepare_snippet(b, sender, subject);
+ b->best_is_new = new;
+ prepare_snippets(b, sender, subject);
}
+ if (content_length >= 0)
+ mb_skip(content_length);
+
int ct = 1;
while (from[ct])
{
done:
close(mb_fd);
+ if (compressed)
+ {
+ int status;
+ if (wait(&status) < 0 || !WIFEXITED(status) || WEXITSTATUS(status))
+ b->total = b->new = b->flagged = -1;
+ }
+}
+
+static time_t
+mdir_mtime(struct mbox *b)
+{
+ time_t mtime = 0;
+ char path[strlen(b->path) + 5];
+
+ for (int new=0; new<2; new++)
+ {
+ sprintf(path, "%s/%s", b->path, (new ? "new" : "cur"));
+ struct stat st;
+ if (stat(path, &st) < 0)
+ debug("[cannot stat %s] ", path);
+ else if (st.st_mtime > mtime)
+ mtime = st.st_mtime;
+ }
+ return mtime;
+}
+
+static void
+mdir_get_snippet(struct mbox *b)
+{
+ char buf[HDR_BUF_SIZE], sender[HDR_BUF_SIZE], subject[HDR_BUF_SIZE];
+ sender[0] = 0;
+ subject[0] = 0;
+
+ char path[strlen(b->path) + MDIR_MAX_NAME_LEN];
+ sprintf(path, "%s/%s", b->path, b->mdir_best);
+ mb_fd = open(path, O_RDONLY);
+ if (mb_fd < 0)
+ {
+ debug("[open failed: %m] ");
+ prepare_snippets(b, sender, subject);
+ return;
+ }
+ mb_seekable = 1;
+ mb_reset(0);
+
+ while (read_hdr_field(buf) > 0)
+ {
+ if (!strncasecmp(buf, "From:", 5))
+ strcpy(sender, buf+5);
+ else if (!strncasecmp(buf, "Subject:", 8))
+ strcpy(subject, buf+8);
+ }
+
+ close(mb_fd);
+ prepare_snippets(b, sender, subject);
+}
+
+static void
+scan_mdir(struct mbox *b)
+{
+ int dir_len = strlen(b->path);
+ char path[dir_len + MDIR_MAX_NAME_LEN];
+ strcpy(path, b->path);
+
+ b->total = b->new = b->flagged = 0;
+ b->best_time = 0;
+ b->best_is_new = 0;
+
+ for (int new=0; new<2; new++)
+ {
+ strcpy(path + dir_len, (new ? "/new" : "/cur"));
+ DIR *d = opendir(path);
+ if (!d)
+ {
+ debug("[cannot open %s: %m] ");
+ continue;
+ }
+ struct dirent *de;
+ while (de = readdir(d))
+ {
+ char *name = de->d_name;
+ if (name[0] == '.' || strlen(name) + 10 > MDIR_MAX_NAME_LEN)
+ continue;
+ path[dir_len + 4] = '/';
+ strcpy(path + dir_len + 5, name);
+
+ char *colon = strchr(name, ':');
+ int seen = 0;
+ int flagged = 0;
+ if (colon && colon[1] == '2' && colon[2] == ',')
+ {
+ // Another comma can separate extended flags (Dovecot extension)
+ for (int i=3; colon[i] && colon[i] != ','; i++)
+ switch (colon[i])
+ {
+ case 'S':
+ seen = 1;
+ break;
+ case 'F':
+ flagged = 1;
+ break;
+ }
+ }
+
+ int is_new = new;
+ if (b->o.unread_is_new && !seen)
+ is_new = 1;
+
+ b->total++;
+ if (is_new)
+ b->new++;
+ if (flagged)
+ b->flagged++;
+ if (debug_mode > 1)
+ debug("%s: new=%d flagged=%d seen=%d\n", path, is_new, flagged, seen);
+
+ if (is_new || flagged)
+ {
+ // Need to pick the best message to display
+ struct stat st;
+ if (stat(path, &st) < 0)
+ debug("[cannot stat %s: %m] ", path);
+ else if (is_new > b->best_is_new || is_new == b->best_is_new && st.st_mtime > b->best_time)
+ {
+ b->best_is_new = is_new;
+ b->best_time = st.st_mtime;
+ strcpy(b->mdir_best, path + dir_len + 1);
+ }
+ }
+ }
+ closedir(d);
+ }
+
+ if (b->best_time && b->o.snippets)
+ {
+ debug("best <%s> ", b->mdir_best);
+ mdir_get_snippet(b);
+ }
}
static void
-scan(void)
+scan(int notify)
{
- debug("Searching for mailboxes...\n");
+ debug("Searching for mailboxes (notify=%d)...\n", notify);
last_scan_time = time(NULL);
CLIST_FOR_EACH(struct pattern_node *, p, patterns)
{
}
}
- rethink_display();
+ rethink_display(0);
debug("Scanning mailboxes...\n");
CLIST_FOR_EACH(struct mbox *, b, mboxes)
debug("inactive\n");
continue;
}
- if (force_refresh)
- b->force_refresh = 1;
+ b->force_refresh = force_refresh;
if (stat(b->path, &st) < 0)
{
- b->total = b->new = -1;
+ b->total = b->new = b->flagged = -1;
debug("%m\n");
+ continue;
+ }
+
+ time_t current_mtime;
+ int current_size;
+ int is_mdir;
+ if (S_ISREG(st.st_mode))
+ {
+ // Regular mailbox
+ is_mdir = 0;
+ current_mtime = st.st_mtime;
+ current_size = st.st_size;
+ debug("[mbox] ");
+ }
+ else if (S_ISDIR(st.st_mode))
+ {
+ // Maildir
+ is_mdir = 1;
+ current_mtime = mdir_mtime(b);
+ current_size = 0;
+ debug("[mdir] ");
}
- else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
+ else
+ {
+ debug("neither file nor directory\n");
+ continue;
+ }
+
+ if (!b->last_time || current_mtime != b->last_time || current_size != b->last_size || b->force_refresh)
{
b->scanning = 1;
redraw_line(b->index);
refresh();
- scan_mbox(b, &st);
- b->last_time = st.st_mtime;
- b->last_size = st.st_size;
- debug("%d %d (stopped at %d of %d)\n", b->total, b->new, b->last_pos, b->last_size);
+ if (is_mdir)
+ scan_mdir(b);
+ else
+ scan_mbox(b, &st);
+ b->last_time = current_mtime;
+ b->last_size = current_size;
+ debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
b->scanning = 0;
redraw_line(b->index);
refresh();
+ b->force_refresh = 0;
+ }
+ else if (b->display_valid_until <= last_scan_time)
+ {
+ debug("not changed, but needs redraw\n");
+ redraw_line(b->index);
}
else
debug("not changed\n");
- b->force_refresh = 0;
}
force_refresh = 0;
debug("Scan finished\n");
last_scan_time = time(NULL);
- rethink_display();
+ rethink_display(notify);
+}
+
+#ifdef CONFIG_X11
+
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+
+static Display *x11_dpy;
+static unsigned leds_care, leds_have, leds_want;
+
+static Atom osd_pty;
+static unsigned osd_care;
+#define OSD_MSG_SIZE 1024
+static char osd_last_msg[OSD_MSG_SIZE];
+static time_t osd_last_time;
+
+static void
+x11_init(void)
+{
+ leds_care = (global_options.led >= 0 ? (1 << global_options.led) : 0);
+ osd_care = (global_options.osd >= 0);
+ CLIST_FOR_EACH(struct option_node *, o, options)
+ {
+ if (o->o.led > 0)
+ leds_care |= (1 << o->o.led);
+ if (o->o.osd > 0)
+ osd_care = 1;
+ }
+
+ if (!leds_care && !osd_care)
+ {
+ debug("X11: No mailbox wants LEDs or OSD\n");
+ return;
+ }
+ if (!getenv("DISPLAY"))
+ {
+ debug("X11: Do not have X display\n");
+ return;
+ }
+ if (!(x11_dpy = XOpenDisplay(NULL)))
+ die("Cannot open X display, although the DISPLAY variable is set");
+
+ if (osd_care)
+ {
+ osd_pty = XInternAtom(x11_dpy, "OSD_QUEUE", False);
+ if (!osd_pty)
+ die("Cannot intern OSD_QUEUE atom");
+
+ // If OSD options contain no message, add one
+ int seen_msg = 0;
+ CLIST_FOR_EACH(struct osd_opt_node *, n, osd_opts)
+ if (!n->key[0])
+ seen_msg = 1;
+ if (!seen_msg)
+ {
+ add_osd_opt("=%40f");
+ add_osd_opt("=%40s");
+ add_osd_opt("=");
+ add_osd_opt("=(and %m more)");
+ }
+ }
+
+ leds_have = ~0U;
+ debug("X11: Initialized\n");
+}
+
+static void
+sync_leds(void)
+{
+ if (leds_want == leds_have)
+ return;
+
+ debug("LEDS: have %02x, want %02x, care %02x\n", leds_have, leds_want, leds_care);
+ for (int i=1; i<10; i++)
+ if (leds_care & (leds_have ^ leds_want) & (1 << i))
+ {
+ XKeyboardControl cc;
+ cc.led = i;
+ cc.led_mode = (leds_want & (1 << i)) ? LedModeOn : LedModeOff;
+ XChangeKeyboardControl(x11_dpy, KBLed | KBLedMode, &cc);
+ }
+ XFlush(x11_dpy);
+ leds_have = leds_want;
}
+static void
+rethink_leds(void)
+{
+ if (!leds_care || !x11_dpy)
+ return;
+
+ leds_want = 0;
+ CLIST_FOR_EACH(struct mbox *, b, mboxes)
+ if (b->o.led > 0 && b->new)
+ leds_want |= (1 << b->o.led);
+ sync_leds();
+}
+
+struct osd_params {
+ struct mbox *mbox;
+ int total_new;
+};
+
+static int
+format_osd_string(char *dest, char *src, struct osd_params *par)
+{
+ char *stop = dest + OSD_MSG_SIZE - 1;
+ char numbuf[16];
+
+ while (*src && dest < stop)
+ {
+ if (*src == '%')
+ {
+ src++;
+ int size = 0;
+ while (*src >= '0' && *src <= '9')
+ size = 10*size + *src++ - '0';
+ if (!size || size > stop-dest)
+ size = dest-stop;
+
+ int spec = *src++;
+ if (!spec)
+ break;
+
+ char *arg = numbuf;
+ switch (spec)
+ {
+ case 'f':
+ arg = par->mbox->sender_snippet;
+ break;
+ case 's':
+ arg = par->mbox->subject_snippet;
+ break;
+ case 'n':
+ snprintf(numbuf, sizeof(numbuf), "%d", par->total_new);
+ break;
+ case 'm':
+ if (par->total_new < 2)
+ return 0;
+ snprintf(numbuf, sizeof(numbuf), "%d", par->total_new - 1);
+ break;
+ case '%':
+ arg = "%";
+ break;
+ default:
+ arg = "???";
+ break;
+ }
+
+ while (*arg && size)
+ {
+ *dest++ = *arg++;
+ size--;
+ }
+ }
+ else
+ *dest++ = *src++;
+ }
+ *dest = 0;
+ return 1;
+}
+
+static void
+format_osd(char *msg, struct osd_params *par)
+{
+ if (!par->mbox)
+ {
+ msg[0] = 0;
+ return;
+ }
+
+ unsigned pos = 0;
+ unsigned have_text = 0;
+ CLIST_FOR_EACH(struct osd_opt_node *, n, osd_opts)
+ {
+ char buf[OSD_MSG_SIZE];
+ if (!format_osd_string(buf, n->val, par))
+ continue;
+ if (!n->key[0] && buf[0])
+ have_text = 1;
+ pos += snprintf(msg+pos, OSD_MSG_SIZE-pos-1, "%s:%s\n", n->key, buf);
+ if (pos > OSD_MSG_SIZE-1)
+ {
+ pos = sprintf(msg, "OSD message too long!\n");
+ break;
+ }
+ }
+ if (have_text)
+ msg[pos++] = '\n';
+ else
+ pos = 0;
+ msg[pos] = 0;
+}
+
+static void
+debug_osd_msg(char *msg)
+{
+ if (!debug_mode)
+ return;
+ fprintf(stderr, "OSD: <");
+ while (*msg)
+ {
+ fputc((*msg != '\n' ? *msg : '|'), stderr);
+ msg++;
+ }
+ fprintf(stderr, ">\n");
+}
+
+static void
+rethink_osd(int notify)
+{
+ if (!osd_care || !x11_dpy || !allow_osd)
+ {
+ osd_last_msg[0] = 0;
+ return;
+ }
+
+ struct osd_params p = { .mbox = NULL, .total_new = 0 };
+ CLIST_FOR_EACH(struct mbox *, b, mboxes)
+ if (b->o.osd > 0)
+ {
+ p.total_new += b->new;
+ if (b->new && b->best_time > osd_last_time && (!p.mbox || p.mbox->o.priority < b->o.priority))
+ p.mbox = b;
+ }
+
+ char new_msg[OSD_MSG_SIZE];
+ format_osd(new_msg, &p);
+ debug_osd_msg(new_msg);
+ if (strcmp(new_msg, osd_last_msg))
+ {
+ strcpy(osd_last_msg, new_msg);
+ if (notify && new_msg[0])
+ {
+ debug("OSD: Sending to daemon\n");
+ XChangeProperty(x11_dpy, DefaultRootWindow(x11_dpy), osd_pty, XA_STRING, 8, PropModeAppend, (unsigned char *) new_msg, strlen(new_msg));
+ XFlush(x11_dpy);
+ }
+ else
+ debug("OSD: No changes\n");
+ osd_last_time = time(NULL);
+ }
+}
+
+static void
+x11_cleanup(void)
+{
+ if (!x11_dpy)
+ return;
+
+ leds_want = 0;
+ sync_leds();
+}
+
+#else
+
+static void x11_init(void) { }
+static void rethink_leds(void) { }
+static void rethink_osd(int notify UNUSED) { }
+static void x11_cleanup(void) { }
+
+#endif
+
static int cursor_at, cursor_max;
+static unsigned is_active, is_pos; // incremental search
+static char is_buf[64];
+
enum {
M_IDLE,
M_SCAN,
M_NEW,
+ M_FLAG,
M_BAD,
+ M_INCSEARCH,
M_MAX
};
-static int attrs[2][2][M_MAX]; // active, hilite, status
+static int attrs[3][2][M_MAX]; // active (2=incsearch), hilite, status
static void
redraw_line(int i)
if (i < cursor_max)
{
struct mbox *b = mbox_array[i];
- int cc = (cursor_at == i);
+ int cc = (cursor_at == i ? (is_active ? 2 : 1) : 0);
int hi = b->o.highlight;
+ unsigned namepos = 0;
+ unsigned namelen = strlen(b->name);
+ int valid = 3600;
attrset(attrs[cc][hi][M_IDLE]);
- if (cc)
+ if (b->o.hotkey)
+ printw("%c ", b->o.hotkey);
+ else if (cc)
printw("> ");
else
printw(" ");
+ if (cc == 2)
+ {
+ attrset(attrs[cc][hi][M_INCSEARCH]);
+ for (namepos=0; namepos < is_pos && namepos < 20; namepos++)
+ addch(is_buf[namepos]);
+ }
if (b->new)
attrset(attrs[cc][hi][M_NEW]);
- printw("%-20s ", b->name);
+ else if (b->flagged && b->o.show_flagged)
+ attrset(attrs[cc][hi][M_FLAG]);
+ else
+ attrset(attrs[cc][hi][M_IDLE]);
+ while (namepos < namelen)
+ addch(b->name[namepos++]);
+ while (namepos++ < 20)
+ addch(' ');
if (b->scanning < 0)
;
else if (b->scanning)
{
attrset(attrs[cc][hi][M_IDLE]);
printw("%6d ", b->total);
+ int snip = 0;
if (b->new)
{
attrset(attrs[cc][hi][M_NEW]);
printw("%6d ", b->new);
attrset(attrs[cc][hi][M_IDLE]);
- int age = (last_scan_time - b->last_time);
+ int age = (last_scan_time - b->best_time);
if (age < 0)
age = 0;
if (age < 3600)
- printw("%2d min ", age/60);
+ {
+ printw("%2d min ", age/60);
+ valid = 60;
+ }
else if (age < 86400)
- printw("%2d hrs ", age/3600);
+ printw("%2d hr%c ", age/3600, (age >= 7200 ? 's' : ' '));
else
printw(" ");
- if (b->o.snippets && b->snippet[0])
+ snip = 1;
+ }
+ else if (b->flagged && b->o.show_flagged)
+ {
+ attrset(attrs[cc][hi][M_FLAG]);
+ printw("%6d ", b->flagged);
+ attrset(attrs[cc][hi][M_IDLE]);
+ printw(" ");
+ attrset(attrs[cc][0][M_FLAG]); /* We avoid the highlight intentionally */
+ snip = 1;
+ }
+ if (snip && b->o.snippets)
+ {
+ int xx, yy;
+ getyx(stdscr, yy, xx);
+ int remains = COLS-1-xx;
+ (void) yy;
+
+ char snip[256];
+ build_snippet(snip, snip + sizeof(snip) - 1, b);
+
+ if (snip[0] && remains > 2)
{
- int xx, yy;
- getyx(stdscr, yy, xx);
- int remains = COLS-1-xx;
- if (remains > 2)
- printw("%-.*s", remains, b->snippet);
+#ifdef CONFIG_WIDE_CURSES
+ size_t len = strlen(snip)+1;
+ wchar_t snip2[len];
+ mbstowcs(snip2, snip, len);
+ addnwstr(snip2, remains);
+#else
+ printw("%-.*s", remains, snip);
+#endif
}
}
}
+ b->display_valid_until = last_scan_time + valid;
}
attrset(attrs[0][0][M_IDLE]);
clrtoeol();
}
static void
-rethink_display(void)
+rethink_display(int notify)
{
int i = 0;
int changed = 0;
redraw_all();
refresh();
}
- if (beeeep && allow_bells)
+ rethink_leds();
+ rethink_osd(notify);
+ if (beeeep && allow_bells && notify)
beep();
}
curs_set(0);
static const int attrs_mono[2][M_MAX] = {
- [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_BAD] = A_DIM },
- [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_BAD] = A_DIM },
+ [0] = { [M_IDLE] = 0,
+ [M_SCAN] = A_BOLD,
+ [M_NEW] = A_BOLD,
+ [M_FLAG] = 0,
+ [M_BAD] = A_DIM,
+ [M_INCSEARCH] = A_REVERSE },
+ [1] = { [M_IDLE] = 0,
+ [M_SCAN] = A_BOLD,
+ [M_NEW] = A_REVERSE | A_BOLD,
+ [M_FLAG] = A_REVERSE,
+ [M_BAD] = A_DIM,
+ [M_INCSEARCH] = A_REVERSE },
};
for (int i=0; i<2; i++)
for (int j=0; j<M_MAX; j++)
{
attrs[0][i][j] = attrs_mono[i][j];
attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
+ attrs[2][i][j] = attrs_mono[i][j] | A_UNDERLINE;
}
if (has_colors())
{
start_color();
- if (COLOR_PAIRS >= 5)
+ if (COLOR_PAIRS >= 12)
{
init_pair(1, COLOR_YELLOW, COLOR_BLACK);
init_pair(2, COLOR_RED, COLOR_BLACK);
init_pair(3, COLOR_WHITE, COLOR_BLUE);
init_pair(4, COLOR_YELLOW, COLOR_BLUE);
init_pair(5, COLOR_RED, COLOR_BLUE);
- static const int attrs_color[2][2][M_MAX] = {
- [0][0] = { [M_IDLE] = 0, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1), [M_BAD] = COLOR_PAIR(2) },
- [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 },
- [1][0] = { [M_IDLE] = COLOR_PAIR(3), [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4), [M_BAD] = COLOR_PAIR(5) },
- [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 },
+ init_pair(6, COLOR_GREEN, COLOR_BLACK);
+ init_pair(7, COLOR_GREEN, COLOR_BLUE);
+ init_pair(8, COLOR_WHITE, COLOR_MAGENTA);
+ init_pair(9, COLOR_YELLOW, COLOR_MAGENTA);
+ init_pair(10, COLOR_GREEN, COLOR_MAGENTA);
+ init_pair(11, COLOR_RED, COLOR_MAGENTA);
+ init_pair(12, COLOR_BLACK, COLOR_YELLOW);
+ static const int attrs_color[3][2][M_MAX] = {
+ [0][0] = { [M_IDLE] = 0,
+ [M_SCAN] = COLOR_PAIR(1),
+ [M_NEW] = COLOR_PAIR(1),
+ [M_FLAG] = COLOR_PAIR(6),
+ [M_BAD] = COLOR_PAIR(2) },
+ [0][1] = { [M_IDLE] = A_BOLD,
+ [M_SCAN] = COLOR_PAIR(1),
+ [M_NEW] = COLOR_PAIR(1) | A_BOLD,
+ [M_FLAG] = COLOR_PAIR(6) | A_BOLD,
+ [M_BAD] = COLOR_PAIR(2) | A_BOLD },
+ [1][0] = { [M_IDLE] = COLOR_PAIR(3),
+ [M_SCAN] = COLOR_PAIR(4),
+ [M_NEW] = COLOR_PAIR(4),
+ [M_FLAG] = COLOR_PAIR(7),
+ [M_BAD] = COLOR_PAIR(5) },
+ [1][1] = { [M_IDLE] = COLOR_PAIR(3) | A_BOLD,
+ [M_SCAN] = COLOR_PAIR(4),
+ [M_NEW] = COLOR_PAIR(4) | A_BOLD,
+ [M_FLAG] = COLOR_PAIR(7) | A_BOLD,
+ [M_BAD] = COLOR_PAIR(5) | A_BOLD },
+ [2][0] = { [M_IDLE] = COLOR_PAIR(8),
+ [M_SCAN] = COLOR_PAIR(9),
+ [M_NEW] = COLOR_PAIR(9),
+ [M_FLAG] = COLOR_PAIR(10),
+ [M_BAD] = COLOR_PAIR(11),
+ [M_INCSEARCH] = COLOR_PAIR(12) | A_DIM },
+ [2][1] = { [M_IDLE] = COLOR_PAIR(8) | A_BOLD,
+ [M_SCAN] = COLOR_PAIR(9),
+ [M_NEW] = COLOR_PAIR(9) | A_BOLD,
+ [M_FLAG] = COLOR_PAIR(10) | A_BOLD,
+ [M_BAD] = COLOR_PAIR(11) | A_BOLD,
+ [M_INCSEARCH] = COLOR_PAIR(12) },
};
memcpy(attrs, attrs_color, sizeof(attrs));
}
}
static void
-scan_and_redraw(void)
+scan_and_redraw(int notify)
{
print_status("Busy...");
- scan();
+ scan(notify);
print_status(NULL);
}
do
{
struct mbox *b = mbox_array[i];
- if (b->new && b->o.priority > bestp)
+ if (simple_tab)
{
- besti = i;
- bestp = b->o.priority;
+ if (b->new)
+ {
+ besti = i;
+ break;
+ }
+ }
+ else
+ {
+ if (b->new && b->o.priority > bestp)
+ {
+ besti = i;
+ bestp = b->o.priority;
+ }
}
i = (i+step) % cursor_max;
}
move_cursor(besti);
}
+static void
+mbox_run(struct mbox *b)
+{
+ char cmd[strlen(run_cmd) + strlen(b->path) + 16];
+ sprintf(cmd, run_cmd, b->path);
+ term_cleanup();
+ system(cmd);
+ term_init();
+ redraw_all();
+ refresh();
+ b->force_refresh = 1;
+ scan_and_redraw(0);
+}
+
+static void
+enter_incsearch(void)
+{
+ print_status("Incremental search...");
+ is_active = 1;
+ is_pos = 0;
+ redraw_line(cursor_at);
+}
+
+static int
+handle_incsearch(int ch)
+{
+ if ((ch == KEY_BACKSPACE || ch == KEY_DC) && is_pos)
+ --is_pos;
+ else if (ch >= ' ' && ch <= '~')
+ {
+ if (is_pos < sizeof(is_buf) - 1)
+ is_buf[is_pos++] = ch;
+ }
+ else
+ {
+ print_status(NULL);
+ is_active = 0;
+ is_pos = 0;
+ redraw_line(cursor_at);
+ return 0;
+ }
+
+ is_buf[is_pos] = 0;
+ for (int i=0; i<cursor_max; i++)
+ {
+ struct mbox *b = mbox_array[i];
+ if (!strncmp(b->name, is_buf, is_pos))
+ {
+ if (i != cursor_at)
+ {
+ move_cursor(i);
+ return 1;
+ }
+ break;
+ }
+ }
+
+ redraw_line(cursor_at);
+ return 1;
+}
+
#define STR2(c) #c
#define STR(c) STR2(c)
-o <pattern>=<opts>\tSet mailbox options\n\
-o <opts>\t\tSet default options for all mailboxes\n\
-p <pri>\t\tSet minimum priority to show\n\
+-s <key>=<val>\t\tSet on-screen display options (consult OSDD docs)\n\
+-t\t\t\tLet TAB select the next mailbox with new mail, no matter what priority it has\n\
\n\
Mailbox options (set with `-o', use upper case to negate):\n\
0-9\t\t\tSet mailbox priority (0=default)\n\
b\t\t\tBeep when a message arrives\n\
+d\t\t\tSend an on-screen-display message (requires OSDD)\n\
e\t\t\tHide from display if empty\n\
+f\t\t\tShow flagged messages if there are no new ones\n\
h\t\t\tHide from display\n\
+l<led>\t\t\tLight a keyboard led (1-9) if running on X display\n\
+m\t\t\tShow mailbox name of the sender\n\
+o\t\t\tCount old, but unread messages as new\n\
+p\t\t\tShow personal info (full name) of the sender\n\
s\t\t\tShow message snippets\n\
t\t\t\tHighlight the entry\n\
+!<key>\t\t\tSet hot key\n\
\n\
CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
It can be freely distributed and used according to the GNU GPL v2.\n\
while (x = *c++)
if (x >= '0' && x <= '9')
o->priority = x - '0';
+ else if (x == '!' && *c)
+ o->hotkey = *c++;
+ else if (x == 'l' && *c >= '1' && *c <= '9')
+ o->led = *c++ - '0';
else
{
int value = !!islower(x);
case 'b':
o->beep = value;
break;
+ case 'd':
+ o->osd = value;
+ break;
case 'e':
o->hide_if_empty = value;
break;
+ case 'f':
+ o->show_flagged = value;
+ break;
case 'h':
o->hide = value;
break;
+ case 'm':
+ o->sender_mbox = value;
+ break;
+ case 'o':
+ o->unread_is_new = value;
+ break;
+ case 'p':
+ o->sender_personal = value;
+ break;
case 's':
o->snippets = value;
break;
clist_init(&mboxes);
clist_init(&options);
clist_init(&patterns);
+ clist_init(&osd_opts);
int c;
- while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
+ while ((c = getopt(argc, argv, "c:dim:o:p:s:t")) >= 0)
switch (c)
{
case 'c':
case 'p':
minimum_priority = atol(optarg);
break;
+ case 's':
+ add_osd_opt(optarg);
+ break;
+ case 't':
+ simple_tab = 1;
+ break;
default:
usage();
}
while (optind < argc)
add_pattern(argv[optind++]);
+ charset_init();
term_init();
- scan_and_redraw();
+ x11_init();
+ scan_and_redraw(0);
next_active(0, 1);
int should_exit = 0;
+restart:
while (!should_exit)
{
time_t now = time(NULL);
int remains = last_scan_time + check_interval - now;
- if (remains <= 0 || force_refresh)
- scan_and_redraw();
+ if (force_refresh)
+ scan_and_redraw(0);
+ if (remains <= 0)
+ scan_and_redraw(1);
else
{
remains *= 10;
halfdelay((remains > 255) ? 255 : remains);
int ch = getch();
+ if (ch < 0)
+ continue;
+ if (is_active && handle_incsearch(ch))
+ {
+ refresh();
+ continue;
+ }
+ for (int i=0; i<num_mboxes; i++)
+ if (ch == mbox_array[i]->o.hotkey)
+ {
+ if (i < cursor_max)
+ cursor_at = i;
+ mbox_run(mbox_array[i]);
+ goto restart;
+ }
switch (ch)
{
case 'q':
case '\r':
case '\n':
if (cursor_at < cursor_max)
- {
- struct mbox *b = mbox_array[cursor_at];
- char cmd[strlen(run_cmd) + strlen(b->path) + 16];
- sprintf(cmd, run_cmd, b->path);
- term_cleanup();
- system(cmd);
- term_init();
- redraw_all();
- refresh();
- b->force_refresh = 1;
- scan_and_redraw();
- }
+ mbox_run(mbox_array[cursor_at]);
break;
case 'l' & 0x1f:
clearok(stdscr, TRUE);
allow_bells = 0;
print_status("Bells and whistles are now disabled. Pssst!");
break;
+ case 'd':
+ allow_osd = 1;
+ print_status("On-screen display is now enabled.");
+ break;
+ case 'D':
+ allow_osd = 0;
+ print_status("On-screen display is now disabled. Watch your step.");
+ break;
+ case '/':
+ enter_incsearch();
+ break;
default:
if (ch >= '0' && ch <= '9')
{
minimum_priority = ch - '0';
- scan_and_redraw();
+ scan_and_redraw(0);
}
else
debug("Pressed unknown key %d\n", ch);
}
}
+ x11_cleanup();
term_cleanup();
return 0;
}