/* copyright 2012 Sascha Kruse and contributors (see LICENSE for licensing information) */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef XINERAMA #include #endif #include #include #include #include "dunst.h" #include "draw.h" #include "dunst_dbus.h" #include "list.h" #include "utils.h" #ifndef STATIC_CONFIG #include "options.h" #endif #define INRECT(x,y,rx,ry,rw,rh) ((x) >= (rx) && (x) < (rx)+(rw) && (y) >= (ry) && (y) < (ry)+(rh)) #define LENGTH(X) (sizeof X / sizeof X[0]) #define MIN(a,b) ((a) < (b) ? (a) : (b)) #define MAX(a,b) ((a) > (b) ? (a) : (b)) #define BUTTONMASK (ButtonPressMask|ButtonReleaseMask) #define FONT_HEIGHT_BORDER 2 #define DEFFONT "Monospace-11" #define MSG 1 #define INFO 2 #define DEBUG 3 /* global variables */ #include "config.h" int height_limit; list *rules = NULL; /* index of colors fit to urgency level */ static ColorSet *colors[3]; static const char *color_strings[2][3]; static Atom utf8; static DC *dc; static Window win; static time_t now; static int visible = False; static screen_info scr; static dimension_t geometry; static XScreenSaverInfo *screensaver_info; static int font_h; static int print_notifications = False; static dimension_t window_dim; int dunst_grab_errored = False; int next_notification_id = 1; int deprecated_mod = False; int deprecated_dunstrc_shortcuts = False; /* notification lists */ list *notification_queue = NULL; /* all new notifications get into here */ list *displayed_notifications = NULL; /* currently displayed notifications */ list *notification_history = NULL; /* history of displayed notifications */ /* misc funtions */ void apply_rules(notification * n); void check_timeouts(void); void draw_notifications(void); char *fix_markup(char *str); void handle_mouse_click(XEvent ev); void handleXEvents(void); void history_pop(void); rule_t *initrule(void); int is_idle(void); void run(void); void setup(void); void update_screen_info(); void usage(int exit_status); void hide_window(); l_node *most_important(list * l); void draw_win(void); void hide_win(void); void move_all_to_history(void); void print_version(void); /* show warning notification */ void warn(const char *text, int urg); void init_shortcut(keyboard_shortcut * shortcut); KeySym string_to_mask(char *str); static void print_notification(notification *n) { printf("{\n"); printf("\tappname: %s\n", n->appname); printf("\tsummary: %s\n", n->summary); printf("\tbody: %s\n", n->body); printf("\ticon: %s\n", n->icon); printf("\turgency: %d\n", n->urgency); printf("\tformatted: %s\n", n->msg); printf("\tid: %d\n", n->id); printf("}\n"); } static int GrabXErrorHandler(Display * display, XErrorEvent * e) { dunst_grab_errored = True; char err_buf[BUFSIZ]; XGetErrorText(display, e->error_code, err_buf, BUFSIZ); fputs(err_buf, stderr); fputs("\n", stderr); if (e->error_code != BadAccess) { exit(EXIT_FAILURE); } return 0; } static void setup_error_handler(void) { dunst_grab_errored = False; XFlush(dc->dpy); XSetErrorHandler(GrabXErrorHandler); } static int tear_down_error_handler(void) { XFlush(dc->dpy); XSync(dc->dpy, False); XSetErrorHandler(NULL); return dunst_grab_errored; } int grab_key(keyboard_shortcut * ks) { if (!ks->is_valid) return 1; Window root; root = RootWindow(dc->dpy, DefaultScreen(dc->dpy)); setup_error_handler(); if (ks->is_valid) XGrabKey(dc->dpy, ks->code, ks->mask, root, True, GrabModeAsync, GrabModeAsync); if (tear_down_error_handler()) { fprintf(stderr, "Unable to grab key \"%s\"\n", ks->str); ks->is_valid = False; return 1; } return 0; } void ungrab_key(keyboard_shortcut * ks) { Window root; root = RootWindow(dc->dpy, DefaultScreen(dc->dpy)); if (ks->is_valid) XUngrabKey(dc->dpy, ks->code, ks->mask, root); } void warn(const char *text, int urg) { notification *n = malloc(sizeof(notification)); if (n == NULL) die("Unable to allocate memory", EXIT_FAILURE); n->appname = strdup("dunst"); n->summary = strdup(text); if (n->summary == NULL) die("Unable to allocate memory", EXIT_FAILURE); n->body = strdup(""); n->icon = strdup(""); n->timeout = 0; n->urgency = urg; n->progress = 0; n->dbus_client = NULL; n->color_strings[ColFG] = NULL; n->color_strings[ColBG] = NULL; init_notification(n, 0); map_win(); } int cmp_notification(void *a, void *b) { if (a == NULL && b == NULL) return 0; else if (a == NULL) return -1; else if (b == NULL) return 1; notification *na = (notification *) a; notification *nb = (notification *) b; if (na->urgency != nb->urgency) { return na->urgency - nb->urgency; } else { return nb->timestamp - na->timestamp; } } l_node *most_important(list * l) { if (l == NULL || l_is_empty(l)) { return NULL; } if (sort) { notification *max; l_node *node_max; notification *data; max = l->head->data; node_max = l->head; for (l_node * iter = l->head; iter; iter = iter->next) { data = (notification *) iter->data; if (cmp_notification(max, data) < 0) { max = data; node_max = iter; } } return node_max; } else { return l->head; } } void apply_rules(notification * n) { if (l_is_empty(rules) || n == NULL) { return; } for (l_node * iter = rules->head; iter; iter = iter->next) { rule_t *r = (rule_t *) iter->data; if ((!r->appname || !fnmatch(r->appname, n->appname, 0)) && (!r->summary || !fnmatch(r->summary, n->summary, 0)) && (!r->body || !fnmatch(r->body, n->body, 0)) && (!r->icon || !fnmatch(r->icon, n->icon, 0))) { n->timeout = r->timeout != -1 ? r->timeout : n->timeout; n->urgency = r->urgency != -1 ? r->urgency : n->urgency; n->color_strings[ColFG] = r->fg ? r->fg : n->color_strings[ColFG]; n->color_strings[ColBG] = r->bg ? r->bg : n->color_strings[ColBG]; n->format = r->format ? r->format : n->format; } } } void check_timeouts(void) { l_node *iter; notification *current; l_node *next; /* nothing to do */ if (l_is_empty(displayed_notifications)) { return; } iter = displayed_notifications->head; while (iter != NULL) { current = (notification *) iter->data; /* don't timeout when user is idle */ if (is_idle()) { current->start = now; iter = iter->next; continue; } /* skip hidden and sticky messages */ if (current->start == 0 || current->timeout == 0) { iter = iter->next; continue; } /* remove old message */ if (difftime(now, current->start) > current->timeout) { /* l_move changes iter->next, so we need to store it beforehand */ next = iter->next; close_notification(current, 1); iter = next; continue; } else { iter = iter->next; } } } void update_lists() { l_node *to_move; notification *n; int limit; check_timeouts(); if (geometry.h == 0) { limit = 0; } else if (geometry.h == 1) { limit = 1; } else if (indicate_hidden) { limit = geometry.h - 1; } else { limit = geometry.h; } /* move notifications from queue to displayed */ while (!l_is_empty(notification_queue)) { if (limit > 0 && l_length(displayed_notifications) >= limit) { /* the list is full */ break; } to_move = most_important(notification_queue); if (!to_move) { return; } n = (notification *) to_move->data; if (!n) return; n->start = now; /* TODO get notifications pushed back into * notification_queue if there's a more important * message waiting there */ l_move(notification_queue, displayed_notifications, to_move); l_sort(displayed_notifications, cmp_notification); } } /* TODO get draw_txt_buf as argument */ int do_word_wrap(char *source, int max_width) { rstrip(source); if (!source || strlen(source) == 0) return 0; char *eol = source; while (True) { if (*eol == '\0') return 1; if (*eol == '\n') { *eol = '\0'; return 1 + do_word_wrap(eol + 1, max_width); } if (*eol == '\\' && *(eol+1) == 'n') { *eol = ' '; *(eol+1) = '\0'; return 1 + do_word_wrap(eol + 2, max_width); } if (word_wrap && max_width > 0) { if (textnw(dc, source, (eol - source) + 1) >= max_width) { /* go back to previous space */ char *space = eol; while (space > source && !isspace(*space)) space--; if (space <= source) { /* whe have a word longer than width, so we * split mid-word. That one letter is * collateral damage */ space = eol; } *space = '\0'; if (*(space + 1) == '\0') return 1; return 1 + do_word_wrap(space + 1, max_width); } } eol++; } } void update_draw_txt_buf(notification * n, int max_width) { rstrip(n->msg); char *msg = n->msg; while(isspace(*msg)) msg++; if (n->draw_txt_buf.txt) free(n->draw_txt_buf.txt); char *buf; /* print dup_count */ if (n->dup_count > 0) { asprintf(&buf, "(%d)", n->dup_count); } else { buf = strdup(""); } /* print msg */ { char *new_buf; asprintf(&new_buf, "%s %s", buf, msg); free(buf); buf = new_buf; } /* print age */ int hours, minutes, seconds; time_t t_delta = now - n->timestamp; if (show_age_threshold >= 0 && t_delta >= show_age_threshold) { hours = t_delta / 3600; minutes = t_delta / 60 % 60; seconds = t_delta % 60; char *new_buf; if (hours > 0) { asprintf(&new_buf, "%s (%dh %dm %ds old)", buf, hours, minutes, seconds); } else if (minutes > 0) { asprintf(&new_buf, "%s (%dm %ds old)", buf, minutes, seconds); } else { asprintf(&new_buf, "%s (%ds old)", buf, seconds); } free(buf); buf = new_buf; } n->draw_txt_buf.line_count = do_word_wrap(buf, max_width); n->draw_txt_buf.txt = buf; } char *draw_txt_get_line(draw_txt * dt, int line) { if (line > dt->line_count) { return NULL; } char *begin = dt->txt; for (int i = 1; i < line; i++) { begin += strlen(begin) + 1; } return begin; } int calculate_x_offset(int line_width, int text_width) { int leftover = line_width - text_width; struct timeval t; float pos; /* If the text is wider than the frame, bouncing is enabled and word_wrap disabled */ if (line_width < text_width && bounce_freq > 0.0001 && !word_wrap) { gettimeofday(&t, NULL); pos = ((t.tv_sec % 100) * 1e6 + t.tv_usec) / (1e6 / bounce_freq); return (1 + sinf(2 * 3.14159 * pos)) * leftover / 2; } switch (align) { case left: return 0; case center: return leftover / 2; case right: return leftover; default: /* this can't happen */ return 0; } } unsigned long calculate_foreground_color(unsigned long source_color) { Colormap cmap = DefaultColormap(dc->dpy, DefaultScreen(dc->dpy)); XColor color; color.pixel = source_color; XQueryColor(dc->dpy, cmap, &color); int c_delta = 10000; /* do we need to darken or brighten the colors? */ int darken = (color.red + color.green + color.blue) / 3 > 65535 / 2; if (darken) { if (color.red - c_delta < 0) color.red = 0; else color.red -= c_delta; if (color.green - c_delta < 0) color.green = 0; else color.green -= c_delta; if (color.blue - c_delta < 0) color.blue = 0; else color.blue -= c_delta; } else { if (color.red + c_delta > 65535) color.red = 65535; else color.red += c_delta; if (color.green + c_delta > 65535) color.green = 65535; else color.green += c_delta; if (color.blue + c_delta > 65535) color.green = 65535; else color.green += c_delta; } color.pixel = 0; XAllocColor(dc->dpy, cmap, &color); return color.pixel; } void draw_win(void) { int width, x, y, height; line_height = MAX(line_height, font_h); update_screen_info(); /* calculate width */ if (geometry.mask & WidthValue && geometry.w == 0) { /* dynamic width */ width = 0; } else if (geometry.mask & WidthValue) { /* fixed width */ width = geometry.w; } else { /* across the screen */ width = scr.dim.w; } /* update draw_txt_bufs and line_cnt */ int line_cnt = 0; for (l_node * iter = displayed_notifications->head; iter; iter = iter->next) { notification *n = (notification *) iter->data; update_draw_txt_buf(n, width); line_cnt += n->draw_txt_buf.line_count; } /* calculate height */ if (geometry.h == 0) { height = line_cnt * line_height; } else { height = MAX(geometry.h, (line_cnt * line_height)); } height += (l_length(displayed_notifications) - 1) * separator_height; /* add "(x more)" */ draw_txt x_more; x_more.txt = NULL; char *print_to; int more = l_length(notification_queue); if (indicate_hidden && more > 0) { int x_more_len = strlen(" ( more) ") + digit_count(more); if (geometry.h != 1) { /* add additional line */ x_more.txt = calloc(x_more_len, sizeof(char)); height += line_height; line_cnt++; print_to = x_more.txt; } else { /* append "(x more)" message to notification text */ notification *n = (notification *) displayed_notifications->head->data; print_to = draw_txt_get_line(&n->draw_txt_buf, n->draw_txt_buf.line_count); for (; *print_to != '\0'; print_to++) ; } snprintf(print_to, x_more_len, "(%d more)", more); } /* if we have a dynamic width, calculate the actual width */ if (width == 0) { for (l_node * iter = displayed_notifications->head; iter; iter = iter->next) { notification *n = (notification *) iter->data; for (int i = 0; i < n->draw_txt_buf.line_count; i++) { char *line = draw_txt_get_line(&n->draw_txt_buf, i+1); assert(line != NULL); width = MAX(width, textw(dc, line)); } } } assert(line_height > 0); assert(font_h > 0); assert(width > 0); assert(height > 0); assert(line_cnt > 0); resizedc(dc, width, height); /* draw buffers */ dc->y = 0; ColorSet *last_color; assert(displayed_notifications->head != NULL); for (l_node * iter = displayed_notifications->head; iter; iter = iter->next) { notification *n = (notification *) iter->data; last_color = n->colors; for (int i = 0; i < n->draw_txt_buf.line_count; i++) { char *line = draw_txt_get_line(&n->draw_txt_buf, i + 1); dc->x = 0; drawrect(dc, 0, 0, width, line_height, True, n->colors->BG); dc->x = calculate_x_offset(width, textw(dc, line)); dc->y += (line_height - font_h) / 2; drawtextn(dc, line, strlen(line), n->colors); dc->y += line_height - ((line_height - font_h) / 2); } /* draw separator */ if (separator_height > 0) { dc -> x = 0; double color; if (sep_color == AUTO) color = calculate_foreground_color(n->colors->BG); else color = n->colors->FG; drawrect(dc, 0, 0, width, separator_height, True, color); dc->y += separator_height; } } /* draw x_more */ if (x_more.txt) { dc->x = 0; drawrect(dc, 0, 0, width, line_height, True, last_color->BG); dc->x = calculate_x_offset(width, textw(dc, x_more.txt)); drawtext(dc, x_more.txt, last_color); } /* calculate window position */ if (geometry.mask & XNegative) { x = (scr.dim.x + (scr.dim.w - width)) + geometry.x; } else { x = scr.dim.x + geometry.x; } if (geometry.mask & YNegative) { y = scr.dim.y + (scr.dim.h + geometry.y) - height; } else { y = scr.dim.y + geometry.y; } /* move and map window */ if (x != window_dim.x || y != window_dim.y || width != window_dim.w || height != window_dim.h) { XResizeWindow(dc->dpy, win, width, height); XMoveWindow(dc->dpy, win, x, y); window_dim.x = x; window_dim.y = y; window_dim.h = height; window_dim.w = width; } mapdc(dc, win, width, height); free(x_more.txt); } char *fix_markup(char *str) { char *replace_buf, *start, *end; if (str == NULL) { return NULL; } str = string_replace(""", "\"", str); str = string_replace("'", "'", str); str = string_replace("&", "&", str); str = string_replace("<", "<", str); str = string_replace(">", ">", str); str = string_replace("\n", " ", str); /* remove tags */ str = string_replace("", "", str); str = string_replace("", "", str); str = string_replace("
", " ", str); str = string_replace("
", " ", str); str = string_replace("
", " ", str); str = string_replace("", "", str); str = string_replace("", "", str); str = string_replace("", "", str); str = string_replace("", "", str); str = string_replace("", "", str); start = strstr(str, ""); if (end != NULL) { replace_buf = strndup(start, end - start + 1); str = string_replace(replace_buf, "", str); free(replace_buf); } } start = strstr(str, ""); if (end != NULL) { replace_buf = strndup(start, end - start + 2); str = string_replace(replace_buf, "", str); free(replace_buf); } } return str; } void handle_mouse_click(XEvent ev) { if (ev.xbutton.button == Button3) { move_all_to_history(); return; } if (ev.xbutton.button == Button1) { int y = 0; notification *n; l_node *iter = displayed_notifications->head; assert(iter); for (; iter; iter = iter->next) { n = (notification *) iter->data; int height = font_h * n->draw_txt_buf.line_count; if (ev.xbutton.y > y && ev.xbutton.y < y + height) break; else y += height; } close_notification(n, 2); } } void handleXEvents(void) { XEvent ev; while (XPending(dc->dpy) > 0) { XNextEvent(dc->dpy, &ev); switch (ev.type) { case Expose: if (ev.xexpose.count == 0) draw_win(); mapdc(dc, win, scr.dim.w, font_h); break; case SelectionNotify: if (ev.xselection.property == utf8) break; case VisibilityNotify: if (ev.xvisibility.state != VisibilityUnobscured) XRaiseWindow(dc->dpy, win); break; case ButtonPress: if (ev.xbutton.window == win) { handle_mouse_click(ev); } break; case KeyPress: if (close_ks.str && XLookupKeysym(&ev.xkey, 0) == close_ks.sym && close_ks.mask == ev.xkey.state) { if (!l_is_empty(displayed_notifications)) { notification *n = (notification *) displayed_notifications->head->data; close_notification(n, 2); } } if (history_ks.str && XLookupKeysym(&ev.xkey, 0) == history_ks.sym && history_ks.mask == ev.xkey.state) { history_pop(); } if (close_all_ks.str && XLookupKeysym(&ev.xkey, 0) == close_all_ks.sym && close_all_ks.mask == ev.xkey.state) { move_all_to_history(); } } } } void move_all_to_history() { l_node *node; notification *n; while (!l_is_empty(displayed_notifications)) { node = displayed_notifications->head; n = (notification *) node->data; close_notification(n, 2); } while (!l_is_empty(notification_queue)) { node = notification_queue->head; n = (notification *) node->data; close_notification(n, 2); } } void history_pop(void) { l_node *iter; notification *data; /* nothing to do */ if (l_is_empty(notification_history)) { return; } for (iter = notification_history->head; iter->next; iter = iter->next) ; data = (notification *) iter->data; data->redisplayed = True; data->start = 0; if (sticky_history) { data->timeout = 0; } l_move(notification_history, notification_queue, iter); if (!visible) { map_win(); } } void free_notification(notification * n) { if (n == NULL) return; free(n->appname); free(n->summary); free(n->body); free(n->icon); free(n->msg); free(n->dbus_client); free(n); } int init_notification(notification * n, int id) { const char *fg = NULL; const char *bg = NULL; if (n == NULL) return -1; n->format = format; apply_rules(n); n->msg = string_replace("%a", n->appname, strdup(n->format)); n->msg = string_replace("%s", n->summary, n->msg); n->msg = string_replace("%i", n->icon, n->msg); n->msg = string_replace("%I", basename(n->icon), n->msg); n->msg = string_replace("%b", n->body, n->msg); if (n->progress) { char pg[10]; sprintf(pg, "[%3d%%]", n->progress-1); n->msg = string_replace("%p", pg, n->msg); } else { n->msg = string_replace("%p", "", n->msg); } n->msg = fix_markup(n->msg); n->dup_count = 0; n->draw_txt_buf.txt = NULL; /* check if n is a duplicate */ for (l_node * iter = notification_queue->head; iter; iter = iter->next) { notification *orig = (notification *) iter->data; if (strcmp(orig->appname, n->appname) == 0 && strcmp(orig->msg, n->msg) == 0) { orig->dup_count++; free_notification(n); return orig->id; } } for (l_node * iter = displayed_notifications->head; iter; iter = iter->next) { notification *orig = (notification *) iter->data; if (strcmp(orig->appname, n->appname) == 0 && strcmp(orig->msg, n->msg) == 0) { orig->dup_count++; orig->start = now; free_notification(n); return orig->id; } } /* urgency > CRIT -> array out of range */ n->urgency = n->urgency > CRIT ? CRIT : n->urgency; if (n->color_strings[ColFG]) { fg = n->color_strings[ColFG]; } else { fg = color_strings[ColFG][n->urgency]; } if (n->color_strings[ColBG]) { bg = n->color_strings[ColBG]; } else { bg = color_strings[ColBG][n->urgency]; } n->colors = initcolor(dc, fg, bg); n->timeout = n->timeout == -1 ? timeouts[n->urgency] : n->timeout; n->start = 0; n->timestamp = now; n->redisplayed = False; if (id == 0) { n->id = ++next_notification_id; } else { close_notification_by_id(id, -1); n->id = id; } if(strlen(n->msg) == 0) { close_notification(n, 2); printf("skipping notification: %s %s\n", n->body, n->summary); } else { l_push(notification_queue, n); } if (print_notifications) print_notification(n); return n->id; } /* * reasons: * -1 -> notification is a replacement, no NotificationClosed signal emitted * 1 -> the notification expired * 2 -> the notification was dismissed by the user_data * 3 -> The notification was closed by a call to CloseNotification */ int close_notification_by_id(int id, int reason) { l_node *iter; notification *target = NULL; for (iter = displayed_notifications->head; iter; iter = iter->next) { notification *n = (notification *) iter->data; if (n->id == id) { l_move(displayed_notifications, notification_history, iter); target = n; break; } } for (iter = notification_queue->head; iter; iter = iter->next) { notification *n = (notification *) iter->data; if (n->id == id) { l_move(notification_queue, notification_history, iter); target = n; break; } } if (reason > 0 && reason < 4 && target != NULL) { notificationClosed(target, reason); } return target == NULL; } int close_notification(notification * n, int reason) { if (n == NULL) return -1; return close_notification_by_id(n->id, reason); } KeySym string_to_mask(char *str) { if (!strcmp(str, "ctrl")) { return ControlMask; } else if (!strcmp(str, "mod4")) { return Mod4Mask; } else if (!strcmp(str, "mod3")) { return Mod3Mask; } else if (!strcmp(str, "mod2")) { return Mod2Mask; } else if (!strcmp(str, "mod1")) { return Mod1Mask; } else if (!strcmp(str, "shift")) { return ShiftMask; } else { fprintf(stderr, "Warning: Unknown Modifier: %s\n", str); return 0; } } void init_shortcut(keyboard_shortcut * ks) { if (ks == NULL || ks->str == NULL) return; if (!strcmp(ks->str, "none") || (!strcmp(ks->str, ""))) { ks->is_valid = False; return; } char *str = strdup(ks->str); char *str_begin = str; if (str == NULL) die("Unable to allocate memory", EXIT_FAILURE); while (strstr(str, "+")) { char *mod = str; while (*str != '+') str++; *str = '\0'; str++; rstrip(mod); ks->mask = ks->mask | string_to_mask(mod); } rstrip(str); ks->sym = XStringToKeysym(str); /* find matching keycode for ks->sym */ int min_keysym, max_keysym; XDisplayKeycodes(dc->dpy, &min_keysym, &max_keysym); ks->code = NoSymbol; int level = ks->mask & ShiftMask ? 1 : 0; for (int i = min_keysym; i <= max_keysym; i++) { if (XkbKeycodeToKeysym(dc->dpy, i, 0, level) == ks->sym) { ks->code = i; break; } } if (ks->sym == NoSymbol || ks->code == NoSymbol) { fprintf(stderr, "Warning: Unknown keyboard shortcut: %s\n", ks->str); ks->is_valid = False; } else { ks->is_valid = True; } free(str_begin); } rule_t *initrule(void) { rule_t *r = malloc(sizeof(rule_t)); r->name = NULL; r->appname = NULL; r->summary = NULL; r->body = NULL; r->icon = NULL; r->timeout = -1; r->urgency = -1; r->fg = NULL; r->bg = NULL; r->format = NULL; return r; } int is_idle(void) { XScreenSaverQueryInfo(dc->dpy, DefaultRootWindow(dc->dpy), screensaver_info); if (idle_threshold == 0) { return False; } return screensaver_info->idle / 1000 > idle_threshold; } void run(void) { while (True) { if (visible) { dbus_poll(50); } else { dbus_poll(200); } now = time(&now); /* move messages from notification_queue to displayed_notifications */ update_lists(); if (l_length(displayed_notifications) > 0) { if (!visible) { map_win(); } else { draw_win(); } } else { if (visible) { hide_win(); } } handleXEvents(); } } void hide_win() { ungrab_key(&close_ks); ungrab_key(&close_all_ks); XUngrabButton(dc->dpy, AnyButton, AnyModifier, win); XUnmapWindow(dc->dpy, win); XFlush(dc->dpy); visible = False; } Window get_focused_window(void) { Window focused = 0; Atom type; int format; unsigned long nitems, bytes_after; unsigned char *prop_return = NULL; Window root = RootWindow(dc->dpy, DefaultScreen(dc->dpy)); Atom netactivewindow = XInternAtom(dc->dpy, "_NET_ACTIVE_WINDOW", False); XGetWindowProperty(dc->dpy, root, netactivewindow, 0L, sizeof(Window), False, XA_WINDOW, &type, &format, &nitems, &bytes_after, &prop_return); if (prop_return) { focused = *(Window *) prop_return; XFree(prop_return); } return focused; } #ifdef XINERAMA int select_screen(XineramaScreenInfo * info, int info_len) { if (f_mode == FOLLOW_NONE) { return scr.scr; } else { int x, y; assert(f_mode == FOLLOW_MOUSE || f_mode == FOLLOW_KEYBOARD); Window root = RootWindow(dc->dpy, DefaultScreen(dc->dpy)); if (f_mode == FOLLOW_MOUSE) { int dummy; unsigned int dummy_ui; Window dummy_win; XQueryPointer(dc->dpy, root, &dummy_win, &dummy_win, &x, &y, &dummy, &dummy, &dummy_ui); } if (f_mode == FOLLOW_KEYBOARD) { Window focused = get_focused_window(); if (focused == 0) { /* something went wrong. Fallback to default */ return scr.scr; } Window child_return; XTranslateCoordinates(dc->dpy, focused, root, 0, 0, &x, &y, &child_return); } for (int i = 0; i < info_len; i++) { if (INRECT(x, y, info[i].x_org, info[i].y_org, info[i].width, info[i].height)) { return i; } } /* something seems to be wrong. Fallback to default */ return scr.scr; } } #endif void update_screen_info() { #ifdef XINERAMA int n; XineramaScreenInfo *info; if ((info = XineramaQueryScreens(dc->dpy, &n))) { int screen = select_screen(info, n); if (screen >= n) { /* invalid monitor, fallback to default */ screen = 0; } scr.dim.x = info[screen].x_org; scr.dim.y = info[screen].y_org; scr.dim.h = info[screen].height; scr.dim.w = info[screen].width; XFree(info); } else #endif { scr.dim.x = 0; scr.dim.y = 0; scr.dim.w = DisplayWidth(dc->dpy, scr.scr); scr.dim.h = DisplayHeight(dc->dpy, scr.scr); } } void setup(void) { Window root; XSetWindowAttributes wa; notification_queue = l_init(); notification_history = l_init(); displayed_notifications = l_init(); if (scr.scr < 0) { scr.scr = DefaultScreen(dc->dpy); } root = RootWindow(dc->dpy, DefaultScreen(dc->dpy)); utf8 = XInternAtom(dc->dpy, "UTF8_STRING", False); /* menu geometry */ font_h = dc->font.height + FONT_HEIGHT_BORDER; update_screen_info(); /* menu window */ wa.override_redirect = True; wa.background_pixmap = ParentRelative; wa.event_mask = ExposureMask | KeyPressMask | VisibilityChangeMask | ButtonPressMask; win = XCreateWindow(dc->dpy, root, scr.dim.x, scr.dim.y, scr.dim.w, font_h, 0, DefaultDepth(dc->dpy, DefaultScreen(dc->dpy)), CopyFromParent, DefaultVisual(dc->dpy, DefaultScreen(dc->dpy)), CWOverrideRedirect | CWBackPixmap | CWEventMask, &wa); transparency = transparency > 100 ? 100 : transparency; setopacity(dc, win, (unsigned long)((100 - transparency) * (0xffffffff/100))); grab_key(&history_ks); } void map_win(void) { /* window is already mapped or there's nothing to show */ if (visible || l_is_empty(displayed_notifications)) { return; } grab_key(&close_ks); grab_key(&close_all_ks); setup_error_handler(); XGrabButton(dc->dpy, AnyButton, AnyModifier, win, False, BUTTONMASK, GrabModeAsync, GrabModeSync, None, None); if (tear_down_error_handler()) { fprintf(stderr, "Unable to grab mouse button(s)\n"); } update_screen_info(); XMapRaised(dc->dpy, win); draw_win(); XFlush(dc->dpy); visible = True; } void parse_follow_mode(const char *mode) { if (strcmp(mode, "mouse") == 0) f_mode = FOLLOW_MOUSE; else if (strcmp(mode, "keyboard") == 0) f_mode = FOLLOW_KEYBOARD; else if (strcmp(mode, "none") == 0) f_mode = FOLLOW_NONE; else { fprintf(stderr, "Warning: unknown follow mode: \"%s\"\n", mode); f_mode = FOLLOW_NONE; } } void parse_cmdline(int argc, char *argv[]) { int c; while (1) { static struct option long_options[] = { {"help", no_argument, NULL, 'h'}, {"fn", required_argument, NULL, 'F'}, {"nb", required_argument, NULL, 'n'}, {"nf", required_argument, NULL, 'N'}, {"lb", required_argument, NULL, 'l'}, {"lf", required_argument, NULL, 'L'}, {"cb", required_argument, NULL, 'c'}, {"cf", required_argument, NULL, 'C'}, {"to", required_argument, NULL, 't'}, {"lto", required_argument, NULL, '0'}, {"nto", required_argument, NULL, '1'}, {"cto", required_argument, NULL, '2'}, {"format", required_argument, NULL, 'f'}, {"key", required_argument, NULL, 'k'}, {"history_key", required_argument, NULL, 'K'}, {"all_key", required_argument, NULL, 'A'}, {"geometry", required_argument, NULL, 'g'}, {"config", required_argument, NULL, 'r'}, {"mod", required_argument, NULL, 'M'}, {"mon", required_argument, NULL, 'm'}, {"ns", no_argument, NULL, 'x'}, {"follow", required_argument, NULL, 'o'}, {"line_height", required_argument, NULL, 'H'}, {"lh", required_argument, NULL, 'H'}, {"print", no_argument, NULL, 'V'}, {"version", no_argument, NULL, 'v'}, {0, 0, 0, 0} }; int option_index = 0; c = getopt_long_only(argc, argv, "bhsv", long_options, &option_index); if (c == -1) { break; } KeySym mod = 0; switch (c) { case 0: break; case 'h': usage(EXIT_SUCCESS); break; case 'F': font = optarg; break; case 'n': normbgcolor = optarg; break; case 'N': normfgcolor = optarg; break; case 'l': lowbgcolor = optarg; break; case 'L': lowfgcolor = optarg; break; case 'c': critbgcolor = optarg; break; case 'C': critfgcolor = optarg; break; case 't': timeouts[0] = atoi(optarg); timeouts[1] = timeouts[0]; break; case '0': timeouts[0] = atoi(optarg); break; case '1': timeouts[1] = atoi(optarg); break; case '2': timeouts[2] = atoi(optarg); break; case 'm': scr.scr = atoi(optarg); break; case 'f': format = optarg; break; case 'M': deprecated_mod = True; mod = string_to_mask(optarg); close_ks.mask = mod; close_all_ks.mask = mod; history_ks.mask = mod; break; case 'k': close_ks.str = optarg; break; case 'K': history_ks.str = optarg; break; case 'A': close_all_ks.str = optarg; break; case 'g': geom = optarg; break; case 's': sort = True; break; case 'r': /* this option is parsed elsewhere. This is just to supress * error message */ break; case 'x': sort = False; break; case 'o': parse_follow_mode(optarg); break; case 'H': line_height = atoi(optarg); break; case 'v': print_version(); break; case 'V': print_notifications = True; break; default: usage(EXIT_FAILURE); break; } } } #ifndef STATIC_CONFIG static rule_t *dunst_rules_find_or_create(const char *section) { l_node *iter; rule_t *rule; /* find rule */ for (iter = rules->head; iter; iter = iter->next) { rule_t *r = (rule_t *) iter->data; if (strcmp(r->name, section) == 0) { return r; } } rule = initrule(); rule->name = strdup(section); l_push(rules, rule); return rule; } void parse_dunstrc(char *cmdline_config_path) { xdgHandle xdg; FILE *config_file = NULL; xdgInitHandle(&xdg); if (cmdline_config_path != NULL) { config_file = fopen(cmdline_config_path, "r"); } if (config_file == NULL) { config_file = xdgConfigOpen("dunst/dunstrc", "r", &xdg); } if (config_file == NULL) { /* Fall back to just "dunstrc", which was used before 2012-06-23 * (before v0.2). */ config_file = xdgConfigOpen("dunstrc", "r", &xdg); if (config_file == NULL) { puts("no dunstrc found -> skipping\n"); xdgWipeHandle(&xdg); return; } } load_ini_file(config_file); font = ini_get_string("global", "font", font); format = ini_get_string("global", "format", format); sort = ini_get_bool("global", "sort", sort); indicate_hidden = ini_get_bool("global", "indicate_hidden", indicate_hidden); word_wrap = ini_get_bool("global", "word_wrap", word_wrap); idle_threshold = ini_get_int("global", "idle_threshold", idle_threshold); monitor = ini_get_int("global", "monitor", monitor); { char *c = ini_get_string("global", "follow", ""); if (strlen(c) > 0) { parse_follow_mode(c); free(c); } } geom = ini_get_string("global", "geometry", geom); line_height = ini_get_int("global", "line_height", line_height); { char *c = ini_get_string("global", "modifier", ""); if (strlen(c) > 0) { deprecated_dunstrc_shortcuts = True; KeySym mod = string_to_mask(c); close_ks.mask = mod; close_all_ks.mask = mod; close_all_ks.mask = mod; free(c); } } close_ks.str = ini_get_string("global", "key", close_ks.str); close_all_ks.str = ini_get_string("global", "key", close_all_ks.str); history_ks.str = ini_get_string("global", "key", history_ks.str); bounce_freq = ini_get_double("global", "bounce_freq", bounce_freq); { char *c = ini_get_string("global", "alignment", ""); if (strlen(c) > 0) { if (strcmp(c, "left") == 0) align = left; else if (strcmp(c, "center") == 0) align = center; else if (strcmp(c, "right") == 0) align = right; else fprintf(stderr, "Warning: unknown allignment\n"); free(c); } } show_age_threshold = ini_get_int("global", "show_age_threshold", show_age_threshold); sticky_history = ini_get_bool("global", "sticky_history", sticky_history); separator_height = ini_get_int("global", "separator_height", separator_height); transparency = ini_get_int("global", "transparency", transparency); { char *c = ini_get_string("global", "separator_color", ""); if (strlen(c) > 0) { if (strcmp(c, "auto") == 0) sep_color = AUTO; else if (strcmp(c, "foreground") == 0) sep_color = FOREGROUND; else fprintf(stderr, "Warning: Unknown separator color\n"); free(c); } } lowbgcolor = ini_get_string("urgency_low", "background", lowbgcolor); lowfgcolor = ini_get_string("urgency_low", "foreground", lowfgcolor); timeouts[LOW] = ini_get_int("urgency_low", "timeout", timeouts[LOW]); normbgcolor = ini_get_string("urgency_normal", "background", normbgcolor); normfgcolor = ini_get_string("urgency_normal", "foreground", normfgcolor); timeouts[NORM] = ini_get_int("urgency_normal", "timeout", timeouts[NORM]); critbgcolor = ini_get_string("urgency_critical", "background", critbgcolor); critfgcolor = ini_get_string("urgency_critical", "foreground", critfgcolor); timeouts[CRIT] = ini_get_int("urgency_critical", "timeout", timeouts[CRIT]); close_ks.str = ini_get_string("shortcuts", "close", close_ks.str); close_all_ks.str = ini_get_string("shortcuts", "close_all", close_all_ks.str); history_ks.str = ini_get_string("shortcuts", "history", history_ks.str); char *cur_section = NULL; for (; cur_section; cur_section = next_section(cur_section)) { if (strcmp(cur_section, "global") == 0 || strcmp(cur_section, "shortcuts") == 0 || strcmp(cur_section, "urgency_low") == 0 || strcmp(cur_section, "urgency_normal") == 0 || strcmp(cur_section, "urgency_critical") == 0) continue; rule_t *current_rule = dunst_rules_find_or_create(cur_section); current_rule->appname = ini_get_string( cur_section, "appname", current_rule->appname); current_rule->summary = ini_get_string( cur_section, "summary", current_rule->summary); current_rule->body = ini_get_string( cur_section, "body", current_rule->body); current_rule->icon = ini_get_string( cur_section, "icon", current_rule->icon); current_rule->timeout = ini_get_int( cur_section, "timeout", current_rule->timeout); { char *urg = ini_get_string(cur_section, "urgency", ""); if (strlen(urg) > 0) { if (strcmp(urg, "low") == 0) current_rule->urgency = LOW; else if (strcmp(urg, "normal") == 0) current_rule->urgency = NORM; else if (strcmp(urg, "critical") == 0) current_rule->urgency = CRIT; else fprintf(stderr, "unknown urgency: %s, ignoring\n", urg); free(urg); } } current_rule->fg = ini_get_string( cur_section, "foreground", current_rule->fg); current_rule->bg = ini_get_string( cur_section, "background", current_rule->bg); current_rule->format = ini_get_string( cur_section, "format", current_rule->format); } fclose(config_file); free_ini(); xdgWipeHandle(&xdg); } char *parse_cmdline_for_config_file(int argc, char *argv[]) { for (int i = 0; i < argc; i++) { if (strstr(argv[i], "-config") != 0) { if (i + 1 == argc) { printf ("Invalid commandline: -config needs argument\n"); } return argv[++i]; } } return NULL; } #endif /* STATIC_CONFIG */ int main(int argc, char *argv[]) { now = time(&now); rules = l_init(); for (int i = 0; i < LENGTH(default_rules); i++) { l_push(rules, &default_rules[i]); } scr.scr = monitor; #ifndef STATIC_CONFIG char *cmdline_config_path; cmdline_config_path = parse_cmdline_for_config_file(argc, argv); parse_dunstrc(cmdline_config_path); #endif parse_cmdline(argc, argv); dc = initdc(); init_shortcut(&close_ks); init_shortcut(&close_all_ks); init_shortcut(&history_ks); geometry.mask = XParseGeometry(geom, &geometry.x, &geometry.y, &geometry.w, &geometry.h); screensaver_info = XScreenSaverAllocInfo(); initdbus(); initfont(dc, font); grab_key(&close_ks); ungrab_key(&close_ks); grab_key(&close_all_ks); ungrab_key(&close_all_ks); grab_key(&history_ks); ungrab_key(&history_ks); colors[LOW] = initcolor(dc, lowfgcolor, lowbgcolor); colors[NORM] = initcolor(dc, normfgcolor, normbgcolor); colors[CRIT] = initcolor(dc, critfgcolor, critbgcolor); color_strings[ColFG][LOW] = lowfgcolor; color_strings[ColFG][NORM] = normfgcolor; color_strings[ColFG][CRIT] = critfgcolor; color_strings[ColBG][LOW] = lowbgcolor; color_strings[ColBG][NORM] = normbgcolor; color_strings[ColBG][CRIT] = critbgcolor; window_dim.x = 0; window_dim.y = 0; window_dim.w = 0; window_dim.h = 0; setup(); if (deprecated_mod) warn("-mod is deprecated. Use \"-key mod+key\" instead\n", CRIT); if (deprecated_dunstrc_shortcuts) warn("You are using deprecated settings. Please update your dunstrc. SEE [shortcuts]", CRIT); run(); return 0; } void usage(int exit_status) { fputs ("usage: dunst [-h/--help] [-v] [-geometry geom] [-lh height] [-fn font] [-format fmt]\n[-nb color] [-nf color] [-lb color] [-lf color] [-cb color] [ -cf color]\n[-to secs] [-lto secs] [-cto secs] [-nto secs] [-key key] [-history_key key] [-all_key key] [-mon n] [-follow none/mouse/keyboard] [-config dunstrc]\n", stderr); exit(exit_status); } void print_version(void) { printf("Dunst - a dmenu-ish notification-daemon, version: %s\n", VERSION); exit(EXIT_SUCCESS); } /* vim: set ts=8 sw=8 tw=0: */