
Put all global variables that store user settings into a struct to keep the namespace clean. Move loading of settings into own file so that only the namespace of settings.c gets polluted with the stuff from config.h
2092 lines
60 KiB
C
2092 lines
60 KiB
C
/* copyright 2012 Sascha Kruse and contributors (see LICENSE for licensing information) */
|
|
|
|
// {{{ INCLUDES
|
|
#define _GNU_SOURCE
|
|
#define XLIB_ILLEGAL_ACCESS
|
|
|
|
#include <assert.h>
|
|
#include <unistd.h>
|
|
#include <time.h>
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
#include <fnmatch.h>
|
|
#include <sys/time.h>
|
|
#include <regex.h>
|
|
#include <math.h>
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <sys/wait.h>
|
|
#include <glib.h>
|
|
#include <X11/Xlib.h>
|
|
#include <X11/XKBlib.h>
|
|
#include <X11/Xatom.h>
|
|
#include <X11/Xutil.h>
|
|
#ifdef XINERAMA
|
|
#include <X11/extensions/Xinerama.h>
|
|
#endif
|
|
#include <X11/extensions/scrnsaver.h>
|
|
|
|
#include "dunst.h"
|
|
#include "draw.h"
|
|
#include "dbus.h"
|
|
#include "utils.h"
|
|
|
|
#include "options.h"
|
|
#include "settings.h"
|
|
|
|
// }}}
|
|
|
|
// {{{ DEFINES
|
|
#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 BUTTONMASK (ButtonPressMask|ButtonReleaseMask)
|
|
#define FONT_HEIGHT_BORDER 2
|
|
|
|
#define DEFFONT "Monospace-11"
|
|
|
|
#ifndef VERSION
|
|
#define VERSION "version info needed"
|
|
#endif
|
|
|
|
#define MSG 1
|
|
#define INFO 2
|
|
#define DEBUG 3
|
|
//}}}
|
|
|
|
// {{{ STRUCTS
|
|
typedef struct _x11_source {
|
|
GSource source;
|
|
Display *dpy;
|
|
Window w;
|
|
} x11_source_t;
|
|
// }}}
|
|
|
|
|
|
// {{{ GLOBALS
|
|
int height_limit;
|
|
int font_h;
|
|
|
|
/* index of colors fit to urgency level */
|
|
static const char *color_strings[2][3];
|
|
static Atom utf8;
|
|
static DC *dc;
|
|
static Window win;
|
|
static bool visible = false;
|
|
static dimension_t geometry;
|
|
static XScreenSaverInfo *screensaver_info;
|
|
static dimension_t window_dim;
|
|
static bool pause_display = false;
|
|
static unsigned long framec;
|
|
static unsigned long sep_custom_col;
|
|
|
|
GMainLoop *mainloop = NULL;
|
|
bool timer_active = false;
|
|
|
|
bool dunst_grab_errored = false;
|
|
bool force_redraw = false;
|
|
|
|
int next_notification_id = 1;
|
|
|
|
/* notification lists */
|
|
GQueue *queue = NULL; /* all new notifications get into here */
|
|
GQueue *displayed = NULL; /* currently displayed notifications */
|
|
GQueue *history = NULL; /* history of displayed notifications */
|
|
GSList *rules = NULL;
|
|
// }}}
|
|
|
|
// {{{ FUNCTION DEFINITIONS
|
|
/* rules */
|
|
void rule_apply(rule_t *r, notification *n);
|
|
void rule_apply_all(notification *n);
|
|
bool rule_matches_notification(rule_t *r, notification *n);
|
|
|
|
/* notifications */
|
|
int notification_cmp(const void *a, const void *b);
|
|
int notification_cmp_data(const void *a, const void *b, void *data);
|
|
void notification_run_script(notification *n);
|
|
int notification_close(notification * n, int reason);
|
|
void notification_print(notification *n);
|
|
char *notification_fix_markup(char *str);
|
|
|
|
/* window */
|
|
void x_win_draw(void);
|
|
void x_win_hide(void);
|
|
void x_win_show(void);
|
|
void x_win_setup(void);
|
|
|
|
/* shortcut */
|
|
void x_shortcut_init(keyboard_shortcut *shortcut);
|
|
void x_shortcut_ungrab(keyboard_shortcut *ks);
|
|
int x_shortcut_grab(keyboard_shortcut *ks);
|
|
KeySym x_shortcut_string_to_mask(const char *str);
|
|
static void x_shortcut_setup_error_handler(void);
|
|
static int x_shortcut_tear_down_error_handler(void);
|
|
|
|
/* X misc */
|
|
void x_handle_click(XEvent ev);
|
|
void x_screen_info(screen_info *scr);
|
|
bool x_is_idle(void);
|
|
void x_setup(void);
|
|
|
|
/* X mainloop */
|
|
static gboolean x_mainloop_fd_prepare(GSource *source, gint *timeout);
|
|
static gboolean x_mainloop_fd_check(GSource *source);
|
|
static gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer user_data);
|
|
|
|
/* misc funtions */
|
|
void check_timeouts(void);
|
|
void history_pop(void);
|
|
void usage(int exit_status);
|
|
void move_all_to_history(void);
|
|
void print_version(void);
|
|
char *extract_urls(const char *str);
|
|
void context_menu(void);
|
|
void wake_up(void);
|
|
void pause_signal_handler(int sig);
|
|
|
|
// }}}
|
|
|
|
|
|
|
|
|
|
// {{{ CONTEXT_MENU
|
|
|
|
/*
|
|
* Exctract all urls from a given string.
|
|
*
|
|
* Return: a string of urls separated by \n
|
|
*
|
|
*/
|
|
char *extract_urls( const char * to_match)
|
|
{ // {{{
|
|
static bool is_initialized = false;
|
|
static regex_t cregex;
|
|
|
|
if (!is_initialized) {
|
|
char *regex = "((http|ftp|https)(://))?(www\\.)?[[:alnum:]_-]+\\.[^[:space:]]+";
|
|
int ret = regcomp(&cregex, regex, REG_EXTENDED|REG_ICASE);
|
|
if (ret != 0) {
|
|
printf("failed to compile regex\n");
|
|
return NULL;
|
|
} else {
|
|
is_initialized = true;
|
|
}
|
|
}
|
|
|
|
char *urls = NULL;
|
|
|
|
const char * p = to_match;
|
|
regmatch_t m;
|
|
|
|
while (1) {
|
|
int nomatch = regexec (&cregex, p, 1, &m, 0);
|
|
if (nomatch) {
|
|
return urls;
|
|
}
|
|
int start;
|
|
int finish;
|
|
if (m.rm_so == -1) {
|
|
break;
|
|
}
|
|
start = m.rm_so + (p - to_match);
|
|
finish = m.rm_eo + (p - to_match);
|
|
|
|
char *match = strndup(to_match+start, finish-start);
|
|
|
|
urls = string_append(urls, match, "\n");
|
|
|
|
p += m.rm_eo;
|
|
}
|
|
return urls;
|
|
}
|
|
|
|
// }}}
|
|
|
|
|
|
/*
|
|
* Open url in browser.
|
|
*
|
|
*/
|
|
void open_browser(const char *url)
|
|
{ // {{{
|
|
int browser_pid1 = fork();
|
|
|
|
if (browser_pid1) {
|
|
int status;
|
|
waitpid(browser_pid1, &status, 0);
|
|
} else {
|
|
int browser_pid2 = fork();
|
|
if (browser_pid2) {
|
|
exit(0);
|
|
} else {
|
|
char *browser_cmd = string_append(settings.browser, url, " ");
|
|
char **cmd = g_strsplit(browser_cmd, " ", 0);
|
|
execvp(cmd[0], cmd);
|
|
}
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
|
|
/*
|
|
* Notify the corresponding client
|
|
* that an action has been invoked
|
|
*/
|
|
void invoke_action(const char *action)
|
|
{ // {{{
|
|
notification *invoked = NULL;
|
|
char *action_identifier = NULL;
|
|
|
|
char *name_begin = strstr(action, "(");
|
|
if (!name_begin) {
|
|
printf("invalid action: %s\n", action);
|
|
return;
|
|
}
|
|
name_begin++;
|
|
|
|
|
|
for (GList *iter = g_queue_peek_head_link(displayed); iter; iter = iter->next) {
|
|
notification *n = iter->data;
|
|
if (g_str_has_prefix(action, n->appname)) {
|
|
if (! n->actions)
|
|
continue;
|
|
|
|
for (int i = 0; i < n->actions->count; i += 2) {
|
|
char *a_identifier = n->actions->actions[i];
|
|
char *name = n->actions->actions[i+1];
|
|
if (g_str_has_prefix(name_begin, name)) {
|
|
invoked = n;
|
|
action_identifier = a_identifier;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (invoked && action_identifier) {
|
|
actionInvoked(invoked, action_identifier);
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Dispatch whatever has been returned
|
|
* by the menu.
|
|
*/
|
|
void dispatch_menu_result(const char *input)
|
|
{ // {{{
|
|
char *maybe_url = extract_urls(input);
|
|
if (maybe_url) {
|
|
open_browser(maybe_url);
|
|
free(maybe_url);
|
|
return;
|
|
}
|
|
|
|
invoke_action(input);
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Open the context menu that let's the user
|
|
* select urls/actions/etc
|
|
*/
|
|
void context_menu(void)
|
|
{ // {{{
|
|
char *dmenu_input = NULL;
|
|
|
|
for (GList *iter = g_queue_peek_head_link(displayed); iter; iter = iter->next) {
|
|
notification *n = iter->data;
|
|
dmenu_input = string_append(dmenu_input, n->urls, "\n");
|
|
if (n->actions)
|
|
dmenu_input = string_append(dmenu_input, n->actions->dmenu_str, "\n");
|
|
}
|
|
|
|
|
|
if (!dmenu_input)
|
|
return;
|
|
|
|
char buf[1024];
|
|
int child_io[2];
|
|
int parent_io[2];
|
|
if (pipe(child_io) != 0) {
|
|
PERR("pipe()", errno);
|
|
return;
|
|
}
|
|
if (pipe(parent_io) != 0) {
|
|
PERR("pipe()", errno);
|
|
return;
|
|
}
|
|
int pid = fork();
|
|
|
|
if (pid == 0) {
|
|
close(child_io[1]);
|
|
close(parent_io[0]);
|
|
close(0);
|
|
if (dup(child_io[0]) == -1) {
|
|
PERR("dup()", errno);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
close(1);
|
|
if (dup(parent_io[1]) == -1) {
|
|
PERR("dup()", errno);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
execvp(settings.dmenu_cmd[0], settings.dmenu_cmd);
|
|
} else {
|
|
close(child_io[0]);
|
|
close(parent_io[1]);
|
|
size_t wlen = strlen(dmenu_input);
|
|
if (write(child_io[1], dmenu_input, wlen) != wlen) {
|
|
PERR("write()", errno);
|
|
}
|
|
close(child_io[1]);
|
|
|
|
size_t len = read(parent_io[0], buf, 1023);
|
|
if (len == 0)
|
|
return;
|
|
buf[len - 1] = '\0';
|
|
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
}
|
|
|
|
close(parent_io[0]);
|
|
|
|
|
|
dispatch_menu_result(buf);
|
|
}
|
|
// }}}
|
|
|
|
|
|
// }}}
|
|
|
|
|
|
// {{{ NOTIFICATION
|
|
|
|
/*
|
|
* print a human readable representation
|
|
* of the given notification to stdout.
|
|
*/
|
|
void notification_print(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);
|
|
if (n->urls) {
|
|
printf("\turls\n");
|
|
printf("\t{\n");
|
|
printf("%s\n", n->urls);
|
|
printf("\t}\n");
|
|
}
|
|
|
|
if (n->actions) {
|
|
printf("\tactions:\n");
|
|
printf("\t{\n");
|
|
for (int i = 0; i < n->actions->count; i += 2) {
|
|
printf("\t\t [%s,%s]\n", n->actions->actions[i], n->actions->actions[i+1]);
|
|
}
|
|
printf("actions_dmenu: %s\n", n->actions->dmenu_str);
|
|
printf("\t]\n");
|
|
}
|
|
printf("\tscript: %s\n", n->script);
|
|
printf("}\n");
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Run the script associated with the
|
|
* given notification.
|
|
*/
|
|
void notification_run_script(notification *n)
|
|
{ // {{{
|
|
if (!n->script || strlen(n->script) < 1)
|
|
return;
|
|
|
|
char *appname = n->appname ? n->appname : "";
|
|
char *summary = n->summary ? n->summary : "";
|
|
char *body = n->body ? n->body : "";
|
|
char *icon = n->icon ? n->icon : "";
|
|
|
|
char *urgency;
|
|
switch (n->urgency) {
|
|
case LOW:
|
|
urgency = "LOW";
|
|
break;
|
|
case NORM:
|
|
urgency = "NORMAL";
|
|
break;
|
|
case CRIT:
|
|
urgency = "CRITICAL";
|
|
break;
|
|
default:
|
|
urgency = "NORMAL";
|
|
break;
|
|
}
|
|
|
|
int pid1 = fork();
|
|
|
|
if (pid1) {
|
|
int status;
|
|
waitpid(pid1, &status, 0);
|
|
} else {
|
|
int pid2 = fork();
|
|
if (pid2) {
|
|
exit(0);
|
|
} else {
|
|
int ret = execlp(n->script, n->script,
|
|
appname,
|
|
summary,
|
|
body,
|
|
icon,
|
|
urgency,
|
|
(char *) NULL
|
|
);
|
|
if (ret != 0) {
|
|
PERR("Unable to run script", errno);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Helper function to compare to given
|
|
* notifications.
|
|
*/
|
|
int notification_cmp(const void *va, const void *vb)
|
|
{ // {{{
|
|
notification *a = (notification*) va;
|
|
notification *b = (notification*) vb;
|
|
|
|
if (!settings.sort)
|
|
return 1;
|
|
|
|
if (a->urgency != b->urgency) {
|
|
return b->urgency - a->urgency;
|
|
} else {
|
|
return a->timestamp - b->timestamp;
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Wrapper for notification_cmp to match glib's
|
|
* compare functions signature.
|
|
*/
|
|
int notification_cmp_data(const void *va, const void *vb, void *data)
|
|
{ // {{{
|
|
return notification_cmp(va, vb);
|
|
}
|
|
// }}}
|
|
|
|
|
|
/*
|
|
* Free the memory used by the given notification.
|
|
*/
|
|
void notification_free(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);
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Strip any markup from text
|
|
*/
|
|
|
|
char *notification_fix_markup(char *str)
|
|
{ // {{{
|
|
char *replace_buf, *start, *end;
|
|
|
|
if (str == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
str = string_replace_all(""", "\"", str);
|
|
str = string_replace_all("'", "'", str);
|
|
str = string_replace_all("&", "&", str);
|
|
str = string_replace_all("<", "<", str);
|
|
str = string_replace_all(">", ">", str);
|
|
|
|
/* remove tags */
|
|
str = string_replace_all("<b>", "", str);
|
|
str = string_replace_all("</b>", "", str);
|
|
str = string_replace_all("<br>", " ", str);
|
|
str = string_replace_all("<br/>", " ", str);
|
|
str = string_replace_all("<br />", " ", str);
|
|
str = string_replace_all("<i>", "", str);
|
|
str = string_replace_all("</i>", "", str);
|
|
str = string_replace_all("<u>", "", str);
|
|
str = string_replace_all("</u>", "", str);
|
|
str = string_replace_all("</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;
|
|
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Initialize the given notification and add it to
|
|
* the queue. Replace notification with id if id > 0.
|
|
*/
|
|
int notification_init(notification * n, int id)
|
|
{ // {{{
|
|
const char *fg = NULL;
|
|
const char *bg = NULL;
|
|
|
|
if (n == NULL)
|
|
return -1;
|
|
|
|
if (strcmp("DUNST_COMMAND_PAUSE", n->summary) == 0) {
|
|
pause_display = true;
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp("DUNST_COMMAND_RESUME", n->summary) == 0) {
|
|
pause_display = false;
|
|
return 0;
|
|
}
|
|
|
|
n->script = NULL;
|
|
|
|
n->format = settings.format;
|
|
|
|
rule_apply_all(n);
|
|
|
|
n->msg = string_replace("%a", n->appname, g_strdup(n->format));
|
|
n->msg = string_replace("%s", n->summary, n->msg);
|
|
if (n->icon) {
|
|
n->msg = string_replace("%I", basename(n->icon), n->msg);
|
|
n->msg = string_replace("%i", 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 = notification_fix_markup(n->msg);
|
|
|
|
while (strstr(n->msg, "\\n") != NULL)
|
|
n->msg = string_replace("\\n", "\n", n->msg);
|
|
|
|
if (settings.ignore_newline)
|
|
while (strstr(n->msg, "\n") != NULL)
|
|
n->msg = string_replace("\n", " ", n->msg);
|
|
|
|
n->msg = g_strstrip(n->msg);
|
|
|
|
|
|
n->dup_count = 0;
|
|
|
|
/* check if n is a duplicate */
|
|
for (GList *iter = g_queue_peek_head_link(queue); iter; iter = iter->next) {
|
|
notification *orig = iter->data;
|
|
if (strcmp(orig->appname, n->appname) == 0
|
|
&& strcmp(orig->msg, n->msg) == 0) {
|
|
orig->dup_count++;
|
|
notification_free(n);
|
|
wake_up();
|
|
return orig->id;
|
|
}
|
|
}
|
|
|
|
for (GList *iter = g_queue_peek_head_link(displayed); iter; iter = iter->next) {
|
|
notification *orig = iter->data;
|
|
if (strcmp(orig->appname, n->appname) == 0
|
|
&& strcmp(orig->msg, n->msg) == 0) {
|
|
orig->dup_count++;
|
|
orig->start = time(NULL);
|
|
notification_free(n);
|
|
wake_up();
|
|
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 ? settings.timeouts[n->urgency] : n->timeout;
|
|
n->start = 0;
|
|
|
|
n->timestamp = time(NULL);
|
|
|
|
n->redisplayed = false;
|
|
|
|
if (id == 0) {
|
|
n->id = ++next_notification_id;
|
|
} else {
|
|
notification_close_by_id(id, -1);
|
|
n->id = id;
|
|
}
|
|
|
|
if (strlen(n->msg) == 0) {
|
|
notification_close(n, 2);
|
|
printf("skipping notification: %s %s\n", n->body, n->summary);
|
|
} else {
|
|
g_queue_insert_sorted(queue, n, notification_cmp_data, NULL);
|
|
}
|
|
|
|
char *tmp = g_strconcat(n->summary, " ", n->body, NULL);
|
|
|
|
n->urls = extract_urls(tmp);
|
|
|
|
|
|
if (n->actions) {
|
|
n->actions->dmenu_str = NULL;
|
|
for (int i = 0; i < n->actions->count; i += 2) {
|
|
char *human_readable = n->actions->actions[i+1];
|
|
printf("debug: %s\n", n->appname);
|
|
printf("debug: %s\n", human_readable);
|
|
char *tmp = g_strdup_printf("%s %s", n->appname, human_readable);
|
|
printf("debug: %s\n", tmp);
|
|
|
|
n->actions->dmenu_str = string_append(n->actions->dmenu_str,
|
|
g_strdup_printf("%s(%s)",
|
|
n->appname,
|
|
human_readable), "\n");
|
|
}
|
|
}
|
|
|
|
free(tmp);
|
|
|
|
|
|
if (settings.print_notifications)
|
|
notification_print(n);
|
|
|
|
return n->id;
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Close the notification that has 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 notification_close_by_id(int id, int reason)
|
|
{ // {{{
|
|
notification *target = NULL;
|
|
|
|
for (GList *iter = g_queue_peek_head_link(displayed); iter; iter = iter->next) {
|
|
notification *n = iter->data;
|
|
if (n->id == id) {
|
|
g_queue_remove(displayed, n);
|
|
g_queue_push_tail(history, n);
|
|
target = n;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (GList *iter = g_queue_peek_head_link(queue); iter; iter = iter->next) {
|
|
notification *n = iter->data;
|
|
if (n->id == id) {
|
|
g_queue_remove(queue, n);
|
|
g_queue_push_tail(history, n);
|
|
target = n;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (reason > 0 && reason < 4 && target != NULL) {
|
|
notificationClosed(target, reason);
|
|
}
|
|
|
|
wake_up();
|
|
return reason;
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Close the given notification. SEE notification_close_by_id.
|
|
*/
|
|
int notification_close(notification * n, int reason)
|
|
{ // {{{
|
|
if (n == NULL)
|
|
return -1;
|
|
return notification_close_by_id(n->id, reason);
|
|
}
|
|
// }}}
|
|
|
|
// }}}
|
|
|
|
|
|
// {{{ RULE
|
|
|
|
/*
|
|
* Apply rule to notification.
|
|
*/
|
|
void rule_apply(rule_t *r, notification *n)
|
|
{ // {{{
|
|
if (r->timeout != -1)
|
|
n->timeout = r->timeout;
|
|
if (r->urgency != -1)
|
|
n->urgency = r->urgency;
|
|
if (r->fg)
|
|
n->color_strings[ColFG] = r->fg;
|
|
if (r->bg)
|
|
n->color_strings[ColBG] = r->bg;
|
|
if (r->format)
|
|
n->format = r->format;
|
|
if (r->script)
|
|
n->script = r->script;
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Check all rules if they match n and apply.
|
|
*/
|
|
void rule_apply_all(notification *n)
|
|
{ // {{{
|
|
for (GSList *iter = rules; iter; iter = iter->next) {
|
|
rule_t *r = iter->data;
|
|
if (rule_matches_notification(r, n)) {
|
|
rule_apply(r, n);
|
|
}
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Initialize rule with default values.
|
|
*/
|
|
void rule_init(rule_t *r)
|
|
{ // {{{
|
|
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;
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Check whether rule should be applied to n.
|
|
*/
|
|
bool rule_matches_notification(rule_t *r, notification *n)
|
|
{ // {{{
|
|
|
|
return ((!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)));
|
|
}
|
|
// }}}
|
|
|
|
// }}}
|
|
|
|
|
|
// {{{ X
|
|
// {{{ X_MAINLOOP
|
|
|
|
/*
|
|
* Helper function to use glib's mainloop mechanic
|
|
* with Xlib
|
|
*/
|
|
static gboolean x_mainloop_fd_prepare(GSource *source, gint *timeout)
|
|
{ // {{{
|
|
*timeout = -1;
|
|
return false;
|
|
}
|
|
// }}}
|
|
|
|
|
|
/*
|
|
* Helper function to use glib's mainloop mechanic
|
|
* with Xlib
|
|
*/
|
|
static gboolean x_mainloop_fd_check(GSource *source)
|
|
{ // {{{
|
|
return XPending(dc->dpy) > 0;
|
|
}
|
|
// }}}
|
|
|
|
|
|
/*
|
|
* Main Dispatcher for XEvents
|
|
*/
|
|
static gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
|
|
{ // {{{
|
|
XEvent ev;
|
|
while (XPending(dc->dpy) > 0) {
|
|
XNextEvent(dc->dpy, &ev);
|
|
switch (ev.type) {
|
|
case Expose:
|
|
if (ev.xexpose.count == 0 && visible) {
|
|
}
|
|
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) {
|
|
x_handle_click(ev);
|
|
}
|
|
break;
|
|
case KeyPress:
|
|
if (settings.close_ks.str
|
|
&& XLookupKeysym(&ev.xkey, 0) == settings.close_ks.sym
|
|
&& settings.close_ks.mask == ev.xkey.state) {
|
|
if (displayed) {
|
|
notification_close(g_queue_peek_head_link(displayed)->data, 2);
|
|
}
|
|
}
|
|
if (settings.history_ks.str
|
|
&& XLookupKeysym(&ev.xkey, 0) == settings.history_ks.sym
|
|
&& settings.history_ks.mask == ev.xkey.state) {
|
|
history_pop();
|
|
}
|
|
if (settings.close_all_ks.str
|
|
&& XLookupKeysym(&ev.xkey, 0) == settings.close_all_ks.sym
|
|
&& settings.close_all_ks.mask == ev.xkey.state) {
|
|
move_all_to_history();
|
|
}
|
|
if (settings.context_ks.str
|
|
&& XLookupKeysym(&ev.xkey, 0) == settings.context_ks.sym
|
|
&& settings.context_ks.mask == ev.xkey.state) {
|
|
context_menu();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
// }}}
|
|
|
|
// }}}
|
|
|
|
// {{{ X_MISC
|
|
|
|
/*
|
|
* Check whether the user is currently idle.
|
|
*/
|
|
bool x_is_idle(void)
|
|
{ // {{{
|
|
XScreenSaverQueryInfo(dc->dpy, DefaultRootWindow(dc->dpy),
|
|
screensaver_info);
|
|
if (settings.idle_threshold == 0) {
|
|
return false;
|
|
}
|
|
return screensaver_info->idle / 1000 > settings.idle_threshold;
|
|
}
|
|
// }}}
|
|
|
|
/* TODO move to x_mainloop_* */
|
|
/*
|
|
* Handle incoming mouse click events
|
|
*/
|
|
void x_handle_click(XEvent ev)
|
|
{ // {{{
|
|
if (ev.xbutton.button == Button3) {
|
|
move_all_to_history();
|
|
|
|
return;
|
|
}
|
|
|
|
if (ev.xbutton.button == Button1) {
|
|
int y = settings.separator_height;
|
|
notification *n = NULL;
|
|
for (GList *iter = g_queue_peek_head_link(displayed); iter; iter = iter->next) {
|
|
n = iter->data;
|
|
int text_h = MAX(font_h, settings.line_height) * n->line_count;
|
|
int padding = 2 * settings.h_padding;
|
|
|
|
int height = text_h + padding;
|
|
|
|
if (ev.xbutton.y > y && ev.xbutton.y < y + height)
|
|
break;
|
|
else
|
|
y += height + settings.separator_height;
|
|
}
|
|
if (n)
|
|
notification_close(n, 2);
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
|
|
|
|
/*
|
|
* Return the window that currently has
|
|
* the keyboard focus.
|
|
*/
|
|
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
|
|
/*
|
|
* Select the screen on which the Window
|
|
* should be displayed.
|
|
*/
|
|
int select_screen(XineramaScreenInfo * info, int info_len)
|
|
{ // {{{
|
|
if (settings.f_mode == FOLLOW_NONE) {
|
|
return settings.monitor >= 0 ? settings.monitor : XDefaultScreen(dc->dpy);
|
|
|
|
} else {
|
|
int x, y;
|
|
assert(settings.f_mode == FOLLOW_MOUSE || settings.f_mode == FOLLOW_KEYBOARD);
|
|
Window root = RootWindow(dc->dpy, DefaultScreen(dc->dpy));
|
|
|
|
if (settings.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 (settings.f_mode == FOLLOW_KEYBOARD) {
|
|
|
|
Window focused = get_focused_window();
|
|
|
|
if (focused == 0) {
|
|
/* something went wrong. Fallback to default */
|
|
return settings.monitor >= 0 ? settings.monitor : XDefaultScreen(dc->dpy);
|
|
}
|
|
|
|
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 settings.monitor >= 0 ? settings.monitor : XDefaultScreen(dc->dpy);
|
|
}
|
|
}
|
|
// }}}
|
|
#endif
|
|
|
|
/*
|
|
* Update the information about the monitor
|
|
* geometry.
|
|
*/
|
|
void x_screen_info(screen_info *scr)
|
|
{ // {{{
|
|
#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;
|
|
|
|
int screen;
|
|
if (settings.monitor >= 0)
|
|
screen = settings.monitor;
|
|
else
|
|
screen = DefaultScreen(dc->dpy);
|
|
|
|
scr->dim.w = DisplayWidth(dc->dpy, screen);
|
|
scr->dim.h = DisplayHeight(dc->dpy, screen);
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
|
|
/*
|
|
* Setup X11 stuff
|
|
*/
|
|
void x_setup(void)
|
|
{ // {{{
|
|
|
|
/* initialize dc, font, keyboard, colors */
|
|
dc = initdc();
|
|
|
|
initfont(dc, settings.font);
|
|
|
|
x_shortcut_init(&settings.close_ks);
|
|
x_shortcut_init(&settings.close_all_ks);
|
|
x_shortcut_init(&settings.history_ks);
|
|
x_shortcut_init(&settings.context_ks);
|
|
|
|
x_shortcut_grab(&settings.close_ks);
|
|
x_shortcut_ungrab(&settings.close_ks);
|
|
x_shortcut_grab(&settings.close_all_ks);
|
|
x_shortcut_ungrab(&settings.close_all_ks);
|
|
x_shortcut_grab(&settings.history_ks);
|
|
x_shortcut_ungrab(&settings.history_ks);
|
|
x_shortcut_grab(&settings.context_ks);
|
|
x_shortcut_ungrab(&settings.context_ks);
|
|
|
|
color_strings[ColFG][LOW] = settings.lowfgcolor;
|
|
color_strings[ColFG][NORM] = settings.normfgcolor;
|
|
color_strings[ColFG][CRIT] = settings.critfgcolor;
|
|
|
|
color_strings[ColBG][LOW] = settings.lowbgcolor;
|
|
color_strings[ColBG][NORM] = settings.normbgcolor;
|
|
color_strings[ColBG][CRIT] = settings.critbgcolor;
|
|
|
|
framec = getcolor(dc, settings.frame_color);
|
|
|
|
if (settings.sep_color == CUSTOM) {
|
|
sep_custom_col = getcolor(dc, settings.sep_custom_color_str);
|
|
} else {
|
|
sep_custom_col = 0;
|
|
}
|
|
|
|
/* parse and set geometry and monitor position */
|
|
if (settings.geom[0] == '-') {
|
|
geometry.negative_width = true;
|
|
settings.geom++;
|
|
} else {
|
|
geometry.negative_width = false;
|
|
}
|
|
|
|
geometry.mask = XParseGeometry(settings.geom,
|
|
&geometry.x, &geometry.y,
|
|
&geometry.w, &geometry.h);
|
|
|
|
|
|
screensaver_info = XScreenSaverAllocInfo();
|
|
|
|
|
|
x_win_setup();
|
|
x_shortcut_grab(&settings.history_ks);
|
|
}
|
|
// }}}
|
|
|
|
|
|
// }}}
|
|
|
|
/* TODO comments and naming */
|
|
// {{{ X_RENDER
|
|
|
|
GSList *do_word_wrap(char *text, int max_width)
|
|
{ // {{{
|
|
|
|
GSList *result = NULL;
|
|
g_strstrip(text);
|
|
|
|
if (!text || strlen(text) == 0)
|
|
return 0;
|
|
|
|
char *begin = text;
|
|
char *end = text;
|
|
|
|
while (true) {
|
|
if (*end == '\0') {
|
|
result = g_slist_append(result, g_strdup(begin));
|
|
break;
|
|
}
|
|
if (*end == '\n') {
|
|
*end = ' ';
|
|
result = g_slist_append(result, g_strndup(begin, end - begin));
|
|
begin = ++end;
|
|
}
|
|
|
|
if (settings.word_wrap && max_width > 0 && textnw(dc, begin, (end - begin) + 1) > max_width) {
|
|
/* find previous space */
|
|
char *space = end;
|
|
while (space > begin && !isspace(*space))
|
|
space--;
|
|
|
|
if (space > begin) {
|
|
end = space;
|
|
}
|
|
result = g_slist_append(result, g_strndup(begin, end - begin));
|
|
begin = ++end;
|
|
}
|
|
end++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
// }}}
|
|
|
|
|
|
char *generate_final_text(notification *n)
|
|
{ // {{{
|
|
char *msg = g_strstrip(n->msg);
|
|
char *buf;
|
|
|
|
/* print dup_count and msg*/
|
|
if (n->dup_count > 0 && (n->actions || n->urls)) {
|
|
buf = g_strdup_printf("(%d%s%s) %s",
|
|
n->dup_count,
|
|
n->actions ? "A" : "",
|
|
n->urls ? "U" : "",
|
|
msg);
|
|
} else if (n->actions || n->urls) {
|
|
buf = g_strdup_printf("(%s%s) %s",
|
|
n->actions ? "A" : "",
|
|
n->urls ? "U" : "",
|
|
msg);
|
|
} else {
|
|
buf = g_strdup(msg);
|
|
}
|
|
|
|
/* print age */
|
|
int hours, minutes, seconds;
|
|
time_t t_delta = time(NULL) - n->timestamp;
|
|
|
|
if (settings.show_age_threshold >= 0 && t_delta >= settings.show_age_threshold) {
|
|
hours = t_delta / 3600;
|
|
minutes = t_delta / 60 % 60;
|
|
seconds = t_delta % 60;
|
|
|
|
char *new_buf;
|
|
if (hours > 0) {
|
|
new_buf = g_strdup_printf("%s (%dh %dm %ds old)", buf, hours,
|
|
minutes, seconds);
|
|
} else if (minutes > 0) {
|
|
new_buf = g_strdup_printf("%s (%dm %ds old)", buf, minutes,
|
|
seconds);
|
|
} else {
|
|
new_buf = g_strdup_printf("%s (%ds old)", buf, seconds);
|
|
}
|
|
|
|
free(buf);
|
|
buf = new_buf;
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
// }}}
|
|
|
|
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 && settings.bounce_freq > 0.0001 && !settings.word_wrap) {
|
|
gettimeofday(&t, NULL);
|
|
pos =
|
|
((t.tv_sec % 100) * 1e6 + t.tv_usec) / (1e6 / settings.bounce_freq);
|
|
return (1 + sinf(2 * 3.14159 * pos)) * leftover / 2;
|
|
}
|
|
switch (settings.align) {
|
|
case left:
|
|
return settings.frame_width + settings.h_padding;
|
|
case center:
|
|
return settings.frame_width + settings.h_padding + (leftover / 2);
|
|
case right:
|
|
return settings.frame_width + settings.h_padding + 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;
|
|
}
|
|
// }}}
|
|
|
|
int calculate_width(void)
|
|
{ // {{{
|
|
screen_info scr;
|
|
x_screen_info(&scr);
|
|
if (geometry.mask & WidthValue && geometry.w == 0) {
|
|
/* dynamic width */
|
|
return 0;
|
|
} else if (geometry.mask & WidthValue) {
|
|
/* fixed width */
|
|
if (geometry.negative_width) {
|
|
return scr.dim.w - geometry.w;
|
|
} else {
|
|
return geometry.w;
|
|
}
|
|
} else {
|
|
/* across the screen */
|
|
return scr.dim.w;
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
void move_and_map(int width, int height)
|
|
{ // {{{
|
|
|
|
int x,y;
|
|
screen_info scr;
|
|
x_screen_info(&scr);
|
|
/* 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);
|
|
|
|
}
|
|
// }}}
|
|
|
|
GSList *generate_render_texts(int width)
|
|
{ // {{{
|
|
GSList *render_texts = NULL;
|
|
|
|
for (GList *iter = g_queue_peek_head_link(displayed); iter; iter = iter->next) {
|
|
render_text *rt = g_malloc(sizeof(render_text));
|
|
|
|
rt->colors = ((notification*)iter->data)->colors;
|
|
char *text = generate_final_text(iter->data);
|
|
rt->lines = do_word_wrap(text, width);
|
|
free(text);
|
|
render_texts = g_slist_append(render_texts, rt);
|
|
}
|
|
|
|
/* add (x more) */
|
|
if (settings.indicate_hidden && queue->length > 0) {
|
|
if (geometry.h != 1) {
|
|
render_text *rt = g_malloc(sizeof(render_text));
|
|
rt->colors = ((render_text *) g_slist_last(render_texts)->data)->colors;
|
|
rt->lines = g_slist_append(NULL, g_strdup_printf("%d more)", queue->length));
|
|
render_texts = g_slist_append(render_texts, rt);
|
|
} else {
|
|
GSList *last_lines = ((render_text *) g_slist_last(render_texts)->data)->lines;
|
|
GSList *last_line = g_slist_last(last_lines);
|
|
char *old = last_line->data;
|
|
char *new = g_strdup_printf("%s (%d more)", old, queue->length);
|
|
free(old);
|
|
last_line->data = new;
|
|
}
|
|
}
|
|
|
|
return render_texts;
|
|
}
|
|
// }}}
|
|
|
|
void free_render_text(void *data) {
|
|
g_slist_free_full(((render_text *) data)->lines, g_free);
|
|
}
|
|
|
|
void free_render_texts(GSList *texts) {
|
|
g_slist_free_full(texts, free_render_text);
|
|
}
|
|
|
|
|
|
// }}}
|
|
|
|
|
|
// {{{ X_WIN
|
|
|
|
void x_win_draw(void)
|
|
{ // {{{
|
|
|
|
int outer_width = calculate_width();
|
|
screen_info scr;
|
|
x_screen_info(&scr);
|
|
|
|
|
|
settings.line_height = MAX(settings.line_height, font_h);
|
|
|
|
int width;
|
|
if (outer_width == 0)
|
|
width = 0;
|
|
else
|
|
width = outer_width - (2 * settings.frame_width) - (2 * settings.h_padding);
|
|
|
|
|
|
GSList *texts = generate_render_texts(width);
|
|
int line_count = 0;
|
|
for (GSList *iter = texts; iter; iter = iter->next) {
|
|
render_text *tmp = iter->data;
|
|
line_count += g_slist_length(tmp->lines);
|
|
}
|
|
|
|
/* if we have a dynamic width, calculate the actual width */
|
|
if (width == 0) {
|
|
for (GSList *iter = texts; iter; iter = iter->next) {
|
|
GSList *lines = ((render_text *) iter->data)->lines;
|
|
for (GSList *iiter = lines; iiter; iiter = iiter->next)
|
|
width = MAX(width, textw(dc, iiter->data));
|
|
}
|
|
outer_width = width + (2 * settings.frame_width) + (2 * settings.h_padding);
|
|
}
|
|
|
|
/* resize dc to correct width */
|
|
|
|
int height = (line_count * settings.line_height)
|
|
+ displayed->length * 2 * settings.padding
|
|
+ ((settings.indicate_hidden && queue->length > 0 && geometry.h != 1) ? 2 * settings.padding : 0)
|
|
+ (settings.separator_height * (displayed->length - 1))
|
|
+ (2 * settings.frame_width);
|
|
|
|
resizedc(dc, outer_width, height);
|
|
|
|
/* draw frame
|
|
* this draws a big box in the frame color which get filled with
|
|
* smaller boxes of the notification colors
|
|
*/
|
|
dc->y = 0;
|
|
dc->x = 0;
|
|
if (settings.frame_width > 0) {
|
|
drawrect(dc, 0, 0, outer_width, height, true, framec);
|
|
}
|
|
|
|
dc->y = settings.frame_width;
|
|
dc->x = settings.frame_width;
|
|
|
|
for (GSList *iter = texts; iter; iter = iter->next) {
|
|
|
|
render_text *cur = iter->data;
|
|
ColorSet *colors = cur->colors;
|
|
|
|
|
|
int line_count = 0;
|
|
bool first_line = true;
|
|
for (GSList *iiter = cur->lines; iiter; iiter = iiter->next) {
|
|
char *line = iiter->data;
|
|
line_count++;
|
|
|
|
int pad = 0;
|
|
bool last_line = iiter->next == NULL;
|
|
|
|
if (first_line && last_line)
|
|
pad = 2*settings.padding;
|
|
else if (first_line || last_line)
|
|
pad = settings.padding;
|
|
|
|
dc->x = settings.frame_width;
|
|
|
|
/* draw background */
|
|
drawrect(dc, 0, 0, width + (2*settings.h_padding), pad + settings.line_height, true, colors->BG);
|
|
|
|
/* draw text */
|
|
dc->x = calculate_x_offset(width, textw(dc, line));
|
|
|
|
dc->y += ((settings.line_height - font_h) / 2);
|
|
dc->y += first_line ? settings.padding : 0;
|
|
|
|
drawtextn(dc, line, strlen(line), colors);
|
|
|
|
dc->y += settings.line_height - ((settings.line_height - font_h) / 2);
|
|
dc->y += last_line ? settings.padding : 0;
|
|
|
|
first_line = false;
|
|
}
|
|
|
|
/* draw separator */
|
|
if (settings.separator_height > 0 && iter->next) {
|
|
dc->x = settings.frame_width;
|
|
double color;
|
|
if (settings.sep_color == AUTO)
|
|
color = calculate_foreground_color(colors->BG);
|
|
else if (settings.sep_color == FOREGROUND)
|
|
color = colors->FG;
|
|
else if (settings.sep_color == FRAME)
|
|
color = framec;
|
|
else {
|
|
/* CUSTOM */
|
|
color = sep_custom_col;
|
|
}
|
|
drawrect(dc, 0, 0, width + (2*settings.h_padding), settings.separator_height, true, color);
|
|
dc->y += settings.separator_height;
|
|
}
|
|
}
|
|
|
|
move_and_map(outer_width, height);
|
|
|
|
free_render_texts(texts);
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Setup the window
|
|
*/
|
|
void x_win_setup(void)
|
|
{ // {{{
|
|
|
|
Window root;
|
|
XSetWindowAttributes wa;
|
|
|
|
window_dim.x = 0;
|
|
window_dim.y = 0;
|
|
window_dim.w = 0;
|
|
window_dim.h = 0;
|
|
|
|
root = RootWindow(dc->dpy, DefaultScreen(dc->dpy));
|
|
utf8 = XInternAtom(dc->dpy, "UTF8_STRING", false);
|
|
font_h = dc->font.height + FONT_HEIGHT_BORDER;
|
|
|
|
wa.override_redirect = true;
|
|
wa.background_pixmap = ParentRelative;
|
|
wa.event_mask =
|
|
ExposureMask | KeyPressMask | VisibilityChangeMask |
|
|
ButtonPressMask;
|
|
|
|
screen_info scr;
|
|
x_screen_info(&scr);
|
|
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);
|
|
settings.transparency = settings.transparency > 100 ? 100 : settings.transparency;
|
|
setopacity(dc, win,
|
|
(unsigned long)((100 - settings.transparency) * (0xffffffff / 100)));
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Show the window and grab shortcuts.
|
|
*/
|
|
void x_win_show(void)
|
|
{ // {{{
|
|
/* window is already mapped or there's nothing to show */
|
|
if (visible || g_queue_is_empty(displayed)) {
|
|
return;
|
|
}
|
|
|
|
x_shortcut_grab(&settings.close_ks);
|
|
x_shortcut_grab(&settings.close_all_ks);
|
|
x_shortcut_grab(&settings.context_ks);
|
|
|
|
x_shortcut_setup_error_handler();
|
|
XGrabButton(dc->dpy, AnyButton, AnyModifier, win, false,
|
|
BUTTONMASK, GrabModeAsync, GrabModeSync, None, None);
|
|
if (x_shortcut_tear_down_error_handler()) {
|
|
fprintf(stderr, "Unable to grab mouse button(s)\n");
|
|
}
|
|
|
|
XMapRaised(dc->dpy, win);
|
|
visible = true;
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Hide the window and ungrab unused keyboard_shortcuts
|
|
*/
|
|
void x_win_hide()
|
|
{ // {{{
|
|
x_shortcut_ungrab(&settings.close_ks);
|
|
x_shortcut_ungrab(&settings.close_all_ks);
|
|
x_shortcut_ungrab(&settings.context_ks);
|
|
|
|
XUngrabButton(dc->dpy, AnyButton, AnyModifier, win);
|
|
XUnmapWindow(dc->dpy, win);
|
|
XFlush(dc->dpy);
|
|
visible = false;
|
|
}
|
|
// }}}
|
|
|
|
// }}}
|
|
|
|
|
|
// {{{ X_SHORTCUT
|
|
|
|
/*
|
|
* Parse a string into a modifier mask.
|
|
*/
|
|
KeySym x_shortcut_string_to_mask(const 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;
|
|
}
|
|
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Error handler for grabbing mouse and keyboard errors.
|
|
*/
|
|
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;
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Setup the Error handler.
|
|
*/
|
|
static void x_shortcut_setup_error_handler(void)
|
|
{ // {{{
|
|
dunst_grab_errored = false;
|
|
|
|
XFlush(dc->dpy);
|
|
XSetErrorHandler(GrabXErrorHandler);
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Tear down the Error handler.
|
|
*/
|
|
static int x_shortcut_tear_down_error_handler(void)
|
|
{ // {{{
|
|
XFlush(dc->dpy);
|
|
XSync(dc->dpy, false);
|
|
XSetErrorHandler(NULL);
|
|
return dunst_grab_errored;
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Grab the given keyboard shortcut.
|
|
*/
|
|
int x_shortcut_grab(keyboard_shortcut *ks)
|
|
{ // {{{
|
|
if (!ks->is_valid)
|
|
return 1;
|
|
Window root;
|
|
root = RootWindow(dc->dpy, DefaultScreen(dc->dpy));
|
|
|
|
x_shortcut_setup_error_handler();
|
|
|
|
if (ks->is_valid)
|
|
XGrabKey(dc->dpy, ks->code, ks->mask, root,
|
|
true, GrabModeAsync, GrabModeAsync);
|
|
|
|
if (x_shortcut_tear_down_error_handler()) {
|
|
fprintf(stderr, "Unable to grab key \"%s\"\n", ks->str);
|
|
ks->is_valid = false;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Ungrab the given keyboard shortcut.
|
|
*/
|
|
void x_shortcut_ungrab(keyboard_shortcut *ks)
|
|
{ // {{{
|
|
Window root;
|
|
root = RootWindow(dc->dpy, DefaultScreen(dc->dpy));
|
|
if (ks->is_valid)
|
|
XUngrabKey(dc->dpy, ks->code, ks->mask, root);
|
|
}
|
|
// }}}
|
|
|
|
/*
|
|
* Initialize the keyboard shortcut.
|
|
*/
|
|
void x_shortcut_init(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 = g_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++;
|
|
g_strchomp(mod);
|
|
ks->mask = ks->mask | x_shortcut_string_to_mask(mod);
|
|
}
|
|
g_strstrip(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;
|
|
|
|
for (int i = min_keysym; i <= max_keysym; i++) {
|
|
if (XkbKeycodeToKeysym(dc->dpy, i, 0, 0) == ks->sym
|
|
|| XkbKeycodeToKeysym(dc->dpy, i, 0, 1) == 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);
|
|
}
|
|
// }}}
|
|
|
|
// }}}
|
|
// }}}
|
|
|
|
|
|
// {{{ RUN
|
|
void check_timeouts(void)
|
|
{ // {{{
|
|
/* nothing to do */
|
|
if (displayed->length == 0)
|
|
return;
|
|
|
|
for (GList *iter = g_queue_peek_head_link(displayed); iter; iter = iter->next) {
|
|
notification *n = iter->data;
|
|
|
|
/* don't timeout when user is idle */
|
|
if (x_is_idle()) {
|
|
n->start = time(NULL);
|
|
continue;
|
|
}
|
|
|
|
/* skip hidden and sticky messages */
|
|
if (n->start == 0 || n->timeout == 0) {
|
|
continue;
|
|
}
|
|
|
|
/* remove old message */
|
|
if (difftime(time(NULL), n->start) > n->timeout) {
|
|
force_redraw = true;
|
|
/* close_notification may conflict with iter, so restart */
|
|
notification_close(n, 1);
|
|
check_timeouts();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
void update_lists()
|
|
{ // {{{
|
|
int limit;
|
|
|
|
check_timeouts();
|
|
|
|
if (pause_display) {
|
|
while (displayed->length > 0) {
|
|
g_queue_insert_sorted(queue, g_queue_pop_head(queue), notification_cmp_data, NULL);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (geometry.h == 0) {
|
|
limit = 0;
|
|
} else if (geometry.h == 1) {
|
|
limit = 1;
|
|
} else if (settings.indicate_hidden) {
|
|
limit = geometry.h - 1;
|
|
} else {
|
|
limit = geometry.h;
|
|
}
|
|
|
|
|
|
/* move notifications from queue to displayed */
|
|
while (queue->length > 0) {
|
|
|
|
if (limit > 0 && displayed->length >= limit) {
|
|
/* the list is full */
|
|
break;
|
|
}
|
|
|
|
force_redraw = true;
|
|
|
|
notification *n = g_queue_pop_head(queue);
|
|
|
|
if (!n)
|
|
return;
|
|
n->start = time(NULL);
|
|
if (!n->redisplayed && n->script) {
|
|
notification_run_script(n);
|
|
}
|
|
|
|
g_queue_insert_sorted(displayed, n, notification_cmp_data, NULL);
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
|
|
void move_all_to_history()
|
|
{ // {{{
|
|
while (displayed->length > 0) {
|
|
notification_close(g_queue_peek_head_link(displayed)->data, 2);
|
|
}
|
|
|
|
notification *n = g_queue_pop_head(queue);
|
|
while (n) {
|
|
g_queue_push_tail(history, n);
|
|
n = g_queue_pop_head(queue);
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
void history_pop(void)
|
|
{ // {{{
|
|
|
|
if (g_queue_is_empty(history))
|
|
return;
|
|
|
|
notification *n = g_queue_pop_tail(history);
|
|
n->redisplayed = true;
|
|
n->start = 0;
|
|
n->timeout = settings.sticky_history ? 0 : n->timeout;
|
|
g_queue_push_head(queue, n);
|
|
|
|
if (!visible) {
|
|
wake_up();
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
void update(void)
|
|
{ // {{{
|
|
time_t last_time = time(&last_time);
|
|
static time_t last_redraw = 0;
|
|
|
|
/* move messages from notification_queue to displayed_notifications */
|
|
update_lists();
|
|
if (displayed->length > 0 && ! visible) {
|
|
x_win_show();
|
|
}
|
|
if (displayed->length == 0 && visible) {
|
|
x_win_hide();
|
|
}
|
|
|
|
if (visible && (force_redraw || time(NULL) - last_redraw > 0)) {
|
|
x_win_draw();
|
|
force_redraw = false;
|
|
last_redraw = time(NULL);
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
void wake_up(void)
|
|
{ // {{{
|
|
force_redraw = true;
|
|
update();
|
|
if (!timer_active) {
|
|
timer_active = true;
|
|
g_timeout_add(1000, run, mainloop);
|
|
}
|
|
}
|
|
// }}}
|
|
|
|
gboolean run(void *data)
|
|
{ // {{{
|
|
|
|
update();
|
|
|
|
if (visible && !timer_active) {
|
|
g_timeout_add(200, run, mainloop);
|
|
timer_active = true;
|
|
}
|
|
|
|
if (!visible && timer_active) {
|
|
timer_active = false;
|
|
/* returning false disables timeout */
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
// }}}
|
|
|
|
//}}}
|
|
|
|
|
|
|
|
|
|
// {{{ MAIN
|
|
int main(int argc, char *argv[])
|
|
{ // {{{
|
|
|
|
history = g_queue_new();
|
|
displayed = g_queue_new();
|
|
queue = g_queue_new();
|
|
|
|
|
|
cmdline_load(argc, argv);
|
|
|
|
if (cmdline_get_bool("-v/-version", false, "Print version")
|
|
|| cmdline_get_bool("--version", false, "Print version")) {
|
|
print_version();
|
|
}
|
|
|
|
char *cmdline_config_path;
|
|
cmdline_config_path =
|
|
cmdline_get_string("-conf/-config", NULL,
|
|
"Path to configuration file");
|
|
load_settings(cmdline_config_path);
|
|
|
|
if (cmdline_get_bool("-h/-help", false, "Print help")
|
|
|| cmdline_get_bool("--help", false, "Print help")) {
|
|
usage(EXIT_SUCCESS);
|
|
}
|
|
|
|
int owner_id = initdbus();
|
|
|
|
x_setup();
|
|
|
|
signal (SIGUSR1, pause_signal_handler);
|
|
signal (SIGUSR2, pause_signal_handler);
|
|
|
|
if (settings.startup_notification) {
|
|
notification *n = malloc(sizeof (notification));
|
|
n->appname = "dunst";
|
|
n->summary = "startup";
|
|
n->body = "dunst is up and running";
|
|
n->progress = 0;
|
|
n->timeout = 10;
|
|
n->urgency = LOW;
|
|
n->icon = NULL;
|
|
n->msg = NULL;
|
|
n->dbus_client = NULL;
|
|
n->color_strings[0] = NULL;
|
|
n->color_strings[1] = NULL;
|
|
n->actions = NULL;
|
|
n->urls = NULL;
|
|
notification_init(n, 0);
|
|
}
|
|
|
|
mainloop = g_main_loop_new(NULL, FALSE);
|
|
|
|
GPollFD dpy_pollfd = {dc->dpy->fd,
|
|
G_IO_IN | G_IO_HUP | G_IO_ERR, 0 };
|
|
|
|
GSourceFuncs x11_source_funcs = {
|
|
x_mainloop_fd_prepare,
|
|
x_mainloop_fd_check,
|
|
x_mainloop_fd_dispatch,
|
|
NULL,
|
|
NULL,
|
|
NULL };
|
|
|
|
GSource *x11_source =
|
|
g_source_new(&x11_source_funcs, sizeof(x11_source_t));
|
|
((x11_source_t*)x11_source)->dpy = dc->dpy;
|
|
((x11_source_t*)x11_source)->w = win;
|
|
g_source_add_poll(x11_source, &dpy_pollfd);
|
|
|
|
g_source_attach(x11_source, NULL);
|
|
|
|
run(NULL);
|
|
g_main_loop_run(mainloop);
|
|
|
|
dbus_tear_down(owner_id);
|
|
|
|
return 0;
|
|
}
|
|
// }}}
|
|
|
|
void pause_signal_handler(int sig)
|
|
{ // {{{
|
|
if (sig == SIGUSR1) {
|
|
pause_display = true;
|
|
}
|
|
if (sig == SIGUSR2) {
|
|
pause_display = false;
|
|
}
|
|
|
|
signal (sig, pause_signal_handler);
|
|
}
|
|
// }}}
|
|
|
|
void usage(int exit_status)
|
|
{ // {{{
|
|
fputs("usage:\n", stderr);
|
|
char *us = cmdline_create_usage();
|
|
fputs(us, stderr);
|
|
fputs("\n", stderr);
|
|
exit(exit_status);
|
|
}
|
|
// }}}
|
|
|
|
void print_version(void)
|
|
{ // {{{
|
|
printf("Dunst - A customizable and lightweight notification-daemon %s\n",
|
|
VERSION);
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
// }}}
|
|
// }}}
|
|
|
|
/* vim: set ts=8 sw=8 tw=0: */
|