<commit_before>/* copyright 2012 <NAME> and contributors (see LICENSE for licensing information) */

#define _GNU_SOURCE
#include <unistd.h>
#include <time.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <fnmatch.h>
#include <getopt.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#ifdef XINERAMA
#include <X11/extensions/Xinerama.h>
#endif
#include <X11/extensions/scrnsaver.h>
#include <basedir.h>
#include <basedir_fs.h>

#include "dunst.h"
#include "draw.h"
#include "dunst_dbus.h"
#include "list.h"
#include "ini.h"

#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

/* structs */
typedef struct _screen_info {
        int scr;
        dimension_t dim;
} screen_info;

typedef struct _notification_buffer {
        char txt[BUFSIZ];
        notification *n;
        int x_offset;
} notification_buffer;

/* global variables */
char *font = "-*-terminus-medium-r-*-*-16-*-*-*-*-*-*-*";
char *normbgcolor = "#1793D1";
char *normfgcolor = "#DDDDDD";
char *critbgcolor = "#ffaaaa";
char *critfgcolor = "#000000";
char *lowbgcolor = "#aaaaff";
char *lowfgcolor = "#000000";
char *format = "%s %b";         /* default format */
int timeouts[] = { 10, 10, 0 }; /* low, normal, critical */

char *geom = "0x0";             /* geometry */
int height_limit;
int sort = True;                /* sort messages by urgency */
int indicate_hidden = True;     /* show count of hidden messages */
char *key_string = NULL;
char *history_key_string = NULL;
KeySym mask = 0;
int idle_threshold = 0;
int show_age_threshold = -1;
enum alignment align = left;
int sticky_history = True;

int verbosity = 0;

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 KeySym key = NoSymbol;
static KeySym history_key = NoSymbol;
static screen_info scr;
static dimension_t geometry;
static XScreenSaverInfo *screensaver_info;
static int font_h;

int next_notification_id = 1;

/* 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);
void dunst_printf(int level, const char *fmt, ...);
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);
char *string_replace(const char *needle, const char *replacement,
                     char *haystack);
char *strtrim(char *str);
void run(void);
void setup(void);
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);

int cmp_notification(void *a, void *b)
{
        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 print_rule(rule_t * r)
{
        dunst_printf(DEBUG, "%s %s %s %s %s %d %d %s %s %s\n",
                     r->name,
                     r->appname,
                     r->summary,
                     r->body,
                     r->icon, r->timeout, r->urgency, r->fg, r->bg, r->format);
}

void print_rules(void)
{
        dunst_printf(DEBUG, "current rules:\n");
        if (l_is_empty(rules)) {
                dunst_printf(DEBUG, "no rules present\n");
                return;
        }
        for (l_node * iter = rules->head; iter; iter = iter->next) {
                print_rule((rule_t *) iter->data);
        }
}

void apply_rules(notification * n)
{
        if (l_is_empty(rules)) {
                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))) {
                        dunst_printf(DEBUG, "matched rule: %s\n", r->name);
                        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);
                n = (notification *) to_move->data;
                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);

        }
}

void draw_win(void)
{
        int width, x, y, height;
        unsigned int len = l_length(displayed_notifications);
        notification_buffer *n_buf;
        dc->x = 0;
        dc->y = 0;
        dc->h = 0;

        /* calculate height */
        if (geometry.h == 0) {
                height = len;
        } else {
                height = MIN(geometry.h, len);
        }

        if (indicate_hidden && geometry.h != 1
            && !l_is_empty(notification_queue)) {
                height++;
        }

        /* initialize and fill buffers */
        n_buf = calloc(height, sizeof(notification_buffer));

        l_node *iter;
        int i;
        for (i = 0, iter = displayed_notifications->head; i < height; i++) {
                memset(n_buf[i].txt, '\0', BUFSIZ);
                n_buf[i].x_offset = 0;
                if (iter) {
                        n_buf[i].n = (notification *) iter->data;
                        if (n_buf[i].n->dup_count > 0) {
                                snprintf(n_buf[i].txt, BUFSIZ, "(%d) %s",
                                         n_buf[i].n->dup_count,
                                         n_buf[i].n->msg);
                        } else {
                                strncpy(n_buf[i].txt, n_buf[i].n->msg, BUFSIZ);
                        }
                        iter = iter->next;
                } else {
                        n_buf[i].n = NULL;
                }
        }

        /* add message age to text */
        if (show_age_threshold >= 0) {
                for (i = 0; i < height; i++) {

                        time_t t_delta;
                        int hours, minutes, seconds;
                        char *end;

                        if (strlen(n_buf[i].txt) < 1)
                                continue;

                        t_delta = now - n_buf[i].n->timestamp;

                        if (t_delta < show_age_threshold)
                                continue;

                        hours = t_delta / 3600;
                        minutes = (t_delta / 60);
                        seconds = (t_delta % 60);

                        for (end = n_buf[i].txt; *end != '\0'; end++) ;
                        if (hours > 0) {
                                snprintf(end, BUFSIZ - strlen(n_buf[i].txt),
                                         " (%dh %dm %ds old)",
                                         hours, minutes, seconds);
                        } else if (minutes > 0) {
                                snprintf(end, BUFSIZ - strlen(n_buf[i].txt),
                                         " (%dm %ds old)", minutes, seconds);
                        } else {
                                snprintf(end, BUFSIZ - strlen(n_buf[i].txt),
                                         " (%ds old)", seconds);
                        }

                }
        }

        /* add "(x more)" */
        if (indicate_hidden && geometry.h != 1
            && !l_is_empty(notification_queue)) {
                snprintf(n_buf[height - 1].txt, BUFSIZ, "(%d more)",
                         l_length(notification_queue));
                /* give this buffer the most important notification in order
                 * to set the colors to it
                 */
                n_buf[height - 1].n = most_important(notification_queue)->data;

        } else if (indicate_hidden && !l_is_empty(notification_queue)) {
                /* append "(x more)" message to notification text */
                char *begin;
                for (begin = n_buf[0].txt; *begin != '\0'; begin++) ;
                snprintf(begin, BUFSIZ - strlen(n_buf[0].txt),
                         " (%d more)", l_length(notification_queue));

        }

        /* calculate width */
        if (geometry.mask & WidthValue && geometry.w == 0) {
                /* dynamic width */
                width = 0;
                for (i = 0; i < height; i++) {
                        width = MAX(width, textw(dc, n_buf[i].txt));
                }
        } else if (geometry.mask & WidthValue) {
                /* fixed width */
                width = geometry.w;
        } else {
                /* across the screen */
                width = scr.dim.w;
        }

        /* calculate offsets for alignment */
        for (int i = 0; i < height; i++) {
                if (strlen(n_buf[i].txt) < 1)
                        continue;

                if (align == right) {
                        n_buf[i].x_offset = width - textw(dc, n_buf[i].txt);
                } else if (align == center) {
                        n_buf[i].x_offset = (width - textw(dc,
                                                           n_buf[i].txt)) / 2;
                }
        }

        /* 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.h + geometry.y) - height * font_h;
        } else {
                y = 0 + geometry.y;
        }

        /* resize window and draw background */
        resizedc(dc, width, height * font_h);
        XResizeWindow(dc->dpy, win, width, height * font_h);
        drawrect(dc, 0, 0, width, height * font_h, True, colors[NORM]->BG);

        /* draw buffers */
        for (int i = 0; i < height; i++) {
                if (strlen(n_buf[i].txt) > 0) {
                        notification *n;
                        n = n_buf[i].n;
                        dc->x = 0;
                        drawrect(dc, 0, 0, width, font_h, True, n->colors->BG);
                        dc->x = n_buf[i].x_offset;
                        drawtext(dc, n_buf[i].txt, n->colors);
                        dc->y += font_h;
                }
        }

        /* move and map window */
        XMoveWindow(dc->dpy, win, x, y);
        mapdc(dc, win, width, height * font_h);

        /* cleanup */
        free(n_buf);
}

void dunst_printf(int level, const char *fmt, ...)
{
        va_list ap;

        if (level > verbosity) {
                return;
        }
        va_start(ap, fmt);
        vfprintf(stderr, fmt, ap);
        va_end(ap);
}

char
*fix_markup(char *str)
{
        char *replace_buf, *start, *end;

        if (str == NULL) {
                return NULL;
        }

        str = string_replace("&quot;", "\"", str);
        str = string_replace("&apos;", "'", str);
        str = string_replace("&amp;", "&", str);
        str = string_replace("&lt;", "<", str);
        str = string_replace("&gt;", ">", str);

        str = string_replace("\n", " ", str);
        /* remove tags */
        str = string_replace("<b>", "", str);
        str = string_replace("</b>", "", str);
        str = string_replace("<br>", " ", str);
        str = string_replace("<br/>", " ", str);
        str = string_replace("<br />", " ", str);
        str = string_replace("<i>", "", str);
        str = string_replace("</i>", "", str);
        str = string_replace("<u>", "", str);
        str = string_replace("</u>", "", str);
        str = string_replace("</a>", "", str);

        start = strstr(str, "<a href");
        if (start != NULL) {
                end = strstr(str, ">");
                if (end != NULL) {
                        replace_buf = strndup(start, end - start + 1);
                        str = string_replace(replace_buf, "", str);
                        free(replace_buf);
                }
        }
        start = strstr(str, "<img src");
        if (start != NULL) {
                end = 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)
{
        l_node *iter = displayed_notifications->head;
        notification *n;
        if (ev.xbutton.button == Button3) {
                move_all_to_history();
        } else if (ev.xbutton.button == Button1) {
                int i = ev.xbutton.y / font_h;
                for (i = i; i > 0; i--) {
                        /* if the user clicks on the "(x more)" message,
                         * keep iter at the last displayed message and
                         * remove that instead
                         */
                        if (iter->next) {
                                iter = iter->next;
                        }
                }
                n = (notification *) iter->data;
                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 (XLookupKeysym(&ev.xkey, 0) == key) {
                                if (!l_is_empty(displayed_notifications)) {
                                        notification *n = (notification *)
                                            displayed_notifications->head->data;
                                        close_notification(n, 2);
                                }
                        }
                        if (XLookupKeysym(&ev.xkey, 0) == history_key) {
                                history_pop();
                        }
                }
        }
}

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) {
        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;
        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);

        n->msg = fix_markup(n->msg);
        n->msg = strtrim(n->msg);

        n->dup_count = 0;

        /* 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->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->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;

        dunst_printf(MSG, "%s\n", n->msg);
        dunst_printf(INFO,
                     "{\n  appname: %s\n  summary: %s\n  body: %s\n  icon: %s\n  urgency: %d\n  timeout: %d\n}",
                     n->appname, n->summary, n->body, n->icon,
                     n->urgency, n->timeout);

        if (id == 0) {
                n->id = ++next_notification_id;
        } else {
                close_notification_by_id(id, -1);
        }

        l_push(notification_queue, 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(displayed_notifications, notification_history,
                               iter);
                        target = n;
                        break;
                }
        }

        if (reason > 0 && reason < 4) {
                notificationClosed(target, reason);
        }

        return target == NULL;
}

int close_notification(notification * n, int reason)
{
        return close_notification_by_id(n->id, reason);
}

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;
}

char *string_replace(const char *needle, const char *replacement,
                     char *haystack)
{
        char *tmp, *start;
        int size;
        start = strstr(haystack, needle);
        if (start == NULL) {
                return haystack;
        }

        size = (strlen(haystack) - strlen(needle)) + strlen(replacement) + 1;
        tmp = calloc(sizeof(char), size);
        memset(tmp, '\0', size);

        strncpy(tmp, haystack, start - haystack);
        tmp[start - haystack] = '\0';

        sprintf(tmp + strlen(tmp), "%s%s", replacement, start + strlen(needle));
        free(haystack);

        if (strstr(tmp, needle)) {
                return string_replace(needle, replacement, tmp);
        } else {
                return tmp;
        }
}

char *strtrim(char *str)
{
        char *end;
        while (isspace(*str))
                str++;

        end = str + strlen(str) - 1;
        while (isspace(*end)) {
                *end = '\0';
                end--;
        }

        return str;

}

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()
{
        XUngrabButton(dc->dpy, AnyButton, AnyModifier, win);
        XUnmapWindow(dc->dpy, win);
        XFlush(dc->dpy);
        visible = False;
}

void setup(void)
{
        Window root;
        XSetWindowAttributes wa;
        KeyCode code;
#ifdef XINERAMA
        int n;
        XineramaScreenInfo *info;
#endif

        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;
#ifdef XINERAMA
        if ((info = XineramaQueryScreens(dc->dpy, &n))) {
                if (scr.scr >= n) {
                        fprintf(stderr, "Monitor %d not found\n", scr.scr);
                        exit(EXIT_FAILURE);
                }
                scr.dim.x = info[scr.scr].x_org;
                scr.dim.y = info[scr.scr].y_org;
                scr.dim.h = info[scr.scr].height;
                scr.dim.w = info[scr.scr].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);
        }
        /* 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);

        /* grab keys */
        if (key != NoSymbol) {
                code = XKeysymToKeycode(dc->dpy, key);
                XGrabKey(dc->dpy, code, mask, root, True, GrabModeAsync,
                         GrabModeAsync);
        }

        if (history_key != NoSymbol) {
                code = XKeysymToKeycode(dc->dpy, history_key);
                XGrabKey(dc->dpy, code, mask, root, True, GrabModeAsync,
                         GrabModeAsync);
        }
}

void map_win(void)
{
        /* window is already mapped or there's nothing to show */
        if (visible || l_is_empty(displayed_notifications)) {
                return;
        }

        XMapRaised(dc->dpy, win);
        XGrabButton(dc->dpy, AnyButton, AnyModifier, win, False,<commit_msg>update screen info before mapping window<commit_after>/* copyright 2012 <NAME> and contributors (see LICENSE for licensing information) */

#define _GNU_SOURCE
#include <unistd.h>
#include <time.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <fnmatch.h>
#include <getopt.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#ifdef XINERAMA
#include <X11/extensions/Xinerama.h>
#endif
#include <X11/extensions/scrnsaver.h>
#include <basedir.h>
#include <basedir_fs.h>

#include "dunst.h"
#include "draw.h"
#include "dunst_dbus.h"
#include "list.h"
#include "ini.h"

#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

/* structs */
typedef struct _screen_info {
        int scr;
        dimension_t dim;
} screen_info;

typedef struct _notification_buffer {
        char txt[BUFSIZ];
        notification *n;
        int x_offset;
} notification_buffer;

/* global variables */
char *font = "-*-terminus-medium-r-*-*-16-*-*-*-*-*-*-*";
char *normbgcolor = "#1793D1";
char *normfgcolor = "#DDDDDD";
char *critbgcolor = "#ffaaaa";
char *critfgcolor = "#000000";
char *lowbgcolor = "#aaaaff";
char *lowfgcolor = "#000000";
char *format = "%s %b";         /* default format */
int timeouts[] = { 10, 10, 0 }; /* low, normal, critical */

char *geom = "0x0";             /* geometry */
int height_limit;
int sort = True;                /* sort messages by urgency */
int indicate_hidden = True;     /* show count of hidden messages */
char *key_string = NULL;
char *history_key_string = NULL;
KeySym mask = 0;
int idle_threshold = 0;
int show_age_threshold = -1;
enum alignment align = left;
int sticky_history = True;

int verbosity = 0;

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 KeySym key = NoSymbol;
static KeySym history_key = NoSymbol;
static screen_info scr;
static dimension_t geometry;
static XScreenSaverInfo *screensaver_info;
static int font_h;

int next_notification_id = 1;

/* 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);
void dunst_printf(int level, const char *fmt, ...);
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);
char *string_replace(const char *needle, const char *replacement,
                     char *haystack);
char *strtrim(char *str);
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);

int cmp_notification(void *a, void *b)
{
        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 print_rule(rule_t * r)
{
        dunst_printf(DEBUG, "%s %s %s %s %s %d %d %s %s %s\n",
                     r->name,
                     r->appname,
                     r->summary,
                     r->body,
                     r->icon, r->timeout, r->urgency, r->fg, r->bg, r->format);
}

void print_rules(void)
{
        dunst_printf(DEBUG, "current rules:\n");
        if (l_is_empty(rules)) {
                dunst_printf(DEBUG, "no rules present\n");
                return;
        }
        for (l_node * iter = rules->head; iter; iter = iter->next) {
                print_rule((rule_t *) iter->data);
        }
}

void apply_rules(notification * n)
{
        if (l_is_empty(rules)) {
                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))) {
                        dunst_printf(DEBUG, "matched rule: %s\n", r->name);
                        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);
                n = (notification *) to_move->data;
                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);

        }
}

void draw_win(void)
{
        int width, x, y, height;
        unsigned int len = l_length(displayed_notifications);
        notification_buffer *n_buf;
        dc->x = 0;
        dc->y = 0;
        dc->h = 0;

        /* calculate height */
        if (geometry.h == 0) {
                height = len;
        } else {
                height = MIN(geometry.h, len);
        }

        if (indicate_hidden && geometry.h != 1
            && !l_is_empty(notification_queue)) {
                height++;
        }

        /* initialize and fill buffers */
        n_buf = calloc(height, sizeof(notification_buffer));

        l_node *iter;
        int i;
        for (i = 0, iter = displayed_notifications->head; i < height; i++) {
                memset(n_buf[i].txt, '\0', BUFSIZ);
                n_buf[i].x_offset = 0;
                if (iter) {
                        n_buf[i].n = (notification *) iter->data;
                        if (n_buf[i].n->dup_count > 0) {
                                snprintf(n_buf[i].txt, BUFSIZ, "(%d) %s",
                                         n_buf[i].n->dup_count,
                                         n_buf[i].n->msg);
                        } else {
                                strncpy(n_buf[i].txt, n_buf[i].n->msg, BUFSIZ);
                        }
                        iter = iter->next;
                } else {
                        n_buf[i].n = NULL;
                }
        }

        /* add message age to text */
        if (show_age_threshold >= 0) {
                for (i = 0; i < height; i++) {

                        time_t t_delta;
                        int hours, minutes, seconds;
                        char *end;

                        if (strlen(n_buf[i].txt) < 1)
                                continue;

                        t_delta = now - n_buf[i].n->timestamp;

                        if (t_delta < show_age_threshold)
                                continue;

                        hours = t_delta / 3600;
                        minutes = (t_delta / 60);
                        seconds = (t_delta % 60);

                        for (end = n_buf[i].txt; *end != '\0'; end++) ;
                        if (hours > 0) {
                                snprintf(end, BUFSIZ - strlen(n_buf[i].txt),
                                         " (%dh %dm %ds old)",
                                         hours, minutes, seconds);
                        } else if (minutes > 0) {
                                snprintf(end, BUFSIZ - strlen(n_buf[i].txt),
                                         " (%dm %ds old)", minutes, seconds);
                        } else {
                                snprintf(end, BUFSIZ - strlen(n_buf[i].txt),
                                         " (%ds old)", seconds);
                        }

                }
        }

        /* add "(x more)" */
        if (indicate_hidden && geometry.h != 1
            && !l_is_empty(notification_queue)) {
                snprintf(n_buf[height - 1].txt, BUFSIZ, "(%d more)",
                         l_length(notification_queue));
                /* give this buffer the most important notification in order
                 * to set the colors to it
                 */
                n_buf[height - 1].n = most_important(notification_queue)->data;

        } else if (indicate_hidden && !l_is_empty(notification_queue)) {
                /* append "(x more)" message to notification text */
                char *begin;
                for (begin = n_buf[0].txt; *begin != '\0'; begin++) ;
                snprintf(begin, BUFSIZ - strlen(n_buf[0].txt),
                         " (%d more)", l_length(notification_queue));

        }

        /* calculate width */
        if (geometry.mask & WidthValue && geometry.w == 0) {
                /* dynamic width */
                width = 0;
                for (i = 0; i < height; i++) {
                        width = MAX(width, textw(dc, n_buf[i].txt));
                }
        } else if (geometry.mask & WidthValue) {
                /* fixed width */
                width = geometry.w;
        } else {
                /* across the screen */
                width = scr.dim.w;
        }

        /* calculate offsets for alignment */
        for (int i = 0; i < height; i++) {
                if (strlen(n_buf[i].txt) < 1)
                        continue;

                if (align == right) {
                        n_buf[i].x_offset = width - textw(dc, n_buf[i].txt);
                } else if (align == center) {
                        n_buf[i].x_offset = (width - textw(dc,
                                                           n_buf[i].txt)) / 2;
                }
        }

        /* 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.h + geometry.y) - height * font_h;
        } else {
                y = 0 + geometry.y;
        }

        /* resize window and draw background */
        resizedc(dc, width, height * font_h);
        XResizeWindow(dc->dpy, win, width, height * font_h);
        drawrect(dc, 0, 0, width, height * font_h, True, colors[NORM]->BG);

        /* draw buffers */
        for (int i = 0; i < height; i++) {
                if (strlen(n_buf[i].txt) > 0) {
                        notification *n;
                        n = n_buf[i].n;
                        dc->x = 0;
                        drawrect(dc, 0, 0, width, font_h, True, n->colors->BG);
                        dc->x = n_buf[i].x_offset;
                        drawtext(dc, n_buf[i].txt, n->colors);
                        dc->y += font_h;
                }
        }

        /* move and map window */
        XMoveWindow(dc->dpy, win, x, y);
        mapdc(dc, win, width, height * font_h);

        /* cleanup */
        free(n_buf);
}

void dunst_printf(int level, const char *fmt, ...)
{
        va_list ap;

        if (level > verbosity) {
                return;
        }
        va_start(ap, fmt);
        vfprintf(stderr, fmt, ap);
        va_end(ap);
}

char
*fix_markup(char *str)
{
        char *replace_buf, *start, *end;

        if (str == NULL) {
                return NULL;
        }

        str = string_replace("&quot;", "\"", str);
        str = string_replace("&apos;", "'", str);
        str = string_replace("&amp;", "&", str);
        str = string_replace("&lt;", "<", str);
        str = string_replace("&gt;", ">", str);

        str = string_replace("\n", " ", str);
        /* remove tags */
        str = string_replace("<b>", "", str);
        str = string_replace("</b>", "", str);
        str = string_replace("<br>", " ", str);
        str = string_replace("<br/>", " ", str);
        str = string_replace("<br />", " ", str);
        str = string_replace("<i>", "", str);
        str = string_replace("</i>", "", str);
        str = string_replace("<u>", "", str);
        str = string_replace("</u>", "", str);
        str = string_replace("</a>", "", str);

        start = strstr(str, "<a href");
        if (start != NULL) {
                end = strstr(str, ">");
                if (end != NULL) {
                        replace_buf = strndup(start, end - start + 1);
                        str = string_replace(replace_buf, "", str);
                        free(replace_buf);
                }
        }
        start = strstr(str, "<img src");
        if (start != NULL) {
                end = 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)
{
        l_node *iter = displayed_notifications->head;
        notification *n;
        if (ev.xbutton.button == Button3) {
                move_all_to_history();
        } else if (ev.xbutton.button == Button1) {
                int i = ev.xbutton.y / font_h;
                for (i = i; i > 0; i--) {
                        /* if the user clicks on the "(x more)" message,
                         * keep iter at the last displayed message and
                         * remove that instead
                         */
                        if (iter->next) {
                                iter = iter->next;
                        }
                }
                n = (notification *) iter->data;
                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 (XLookupKeysym(&ev.xkey, 0) == key) {
                                if (!l_is_empty(displayed_notifications)) {
                                        notification *n = (notification *)
                                            displayed_notifications->head->data;
                                        close_notification(n, 2);
                                }
                        }
                        if (XLookupKeysym(&ev.xkey, 0) == history_key) {
                                history_pop();
                        }
                }
        }
}

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) {
        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;
        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);

        n->msg = fix_markup(n->msg);
        n->msg = strtrim(n->msg);

        n->dup_count = 0;

        /* 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->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->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;

        dunst_printf(MSG, "%s\n", n->msg);
        dunst_printf(INFO,
                     "{\n  appname: %s\n  summary: %s\n  body: %s\n  icon: %s\n  urgency: %d\n  timeout: %d\n}",
                     n->appname, n->summary, n->body, n->icon,
                     n->urgency, n->timeout);

        if (id == 0) {
                n->id = ++next_notification_id;
        } else {
                close_notification_by_id(id, -1);
        }

        l_push(notification_queue, 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(displayed_notifications, notification_history,
                               iter);
                        target = n;
                        break;
                }
        }

        if (reason > 0 && reason < 4) {
                notificationClosed(target, reason);
        }

        return target == NULL;
}

int close_notification(notification * n, int reason)
{
        return close_notification_by_id(n->id, reason);
}

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;
}

char *string_replace(const char *needle, const char *replacement,
                     char *haystack)
{
        char *tmp, *start;
        int size;
        start = strstr(haystack, needle);
        if (start == NULL) {
                return haystack;
        }

        size = (strlen(haystack) - strlen(needle)) + strlen(replacement) + 1;
        tmp = calloc(sizeof(char), size);
        memset(tmp, '\0', size);

        strncpy(tmp, haystack, start - haystack);
        tmp[start - haystack] = '\0';

        sprintf(tmp + strlen(tmp), "%s%s", replacement, start + strlen(needle));
        free(haystack);

        if (strstr(tmp, needle)) {
                return string_replace(needle, replacement, tmp);
        } else {
                return tmp;
        }
}

char *strtrim(char *str)
{
        char *end;
        while (isspace(*str))
                str++;

        end = str + strlen(str) - 1;
        while (isspace(*end)) {
                *end = '\0';
                end--;
        }

        return str;

}

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()
{
        XUngrabButton(dc->dpy, AnyButton, AnyModifier, win);
        XUnmapWindow(dc->dpy, win);
        XFlush(dc->dpy);
        visible = False;
}

void update_screen_info()
{
#ifdef XINERAMA
        int n;
        XineramaScreenInfo *info;
#endif
#ifdef XINERAMA
        if ((info = XineramaQueryScreens(dc->dpy, &n))) {
                if (scr.scr >= n) {
                        fprintf(stderr, "Monitor %d not found\n", scr.scr);
                        exit(EXIT_FAILURE);
                }
                scr.dim.x = info[scr.scr].x_org;
                scr.dim.y = info[scr.scr].y_org;
                scr.dim.h = info[scr.scr].height;
                scr.dim.w = info[scr.scr].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;
        KeyCode code;

        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);

        /* grab keys */
        if (key != NoSymbol) {
                code = XKeysymToKeycode(dc->dpy, key);
                XGrabKey(dc->dpy, code, mask, root, True, GrabModeAsync,
                         GrabModeAsync);
        }

        if (history_key != NoSymbol) {
                code = XKeysymToKeycode(dc->dpy, history_key);
                XGrabKey(dc->dpy, code, mask, root, True, GrabModeAsync,
                         GrabModeAsync);
        }
}

void map_win(void)
{
        /* window is already mapped or there's nothing to show */
        if (visible || l_is_empty(displayed_notifications)) {
                return;
        }

        update_screen_info();

        XMapRaised(dc->dpy, win);
        XGrabButton(dc->dpy, AnyButton, AnyModifier, win, False,
{'fraction_non_alphanumeric': 0.1270230920295253, 'fraction_numerical': 0.005688359179251033, 'mean_word_length': 1.3009114989093176, 'pattern_counts': {'":': 0, '<': 89, '<?xml version=': 0, '>': 569, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 48, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7256732', 'n_tokens_mistral': 17534, 'n_tokens_neox': 16420, 'n_words': 3874}