/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include "dbus.h" #include "x.h" #include "notification.h" #include "dunst.h" #include "utils.h" #include "settings.h" #include "rules.h" #include "menu.h" int next_notification_id = 1; /* * 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("\tcategory: %s\n", n->category); printf("\turgency: %d\n", n->urgency); printf("\tformatted: '%s'\n", n->msg); printf("\tfg: %s\n", n->color_strings[ColFG]); printf("\tbg: %s\n", n->color_strings[ColBG]); printf("\tframe: %s\n", n->color_strings[ColFrame]); 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->id - b->id; } } /* * 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); if (n->category && *n->category != '\0') g_free(n->category); if (n->text_to_render) g_free(n->text_to_render); if (n->urls) g_free(n->urls); if (n->actions) { g_strfreev(n->actions->actions); free(n->actions->dmenu_str); } if (n->raw_icon) { if (n->raw_icon->data) free(n->raw_icon->data); free(n->raw_icon); } free(n); } /* * Strip any markup from text */ char *notification_strip_markup(char *str) { if (str == NULL) { return NULL; } /* strip all tags */ string_strip_delimited(str, '<', '>'); /* unquote the remainder */ 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); return str; } /* * Quote a text string for rendering with pango */ char *notification_quote_markup(char *str) { 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); return str; } /* * Replace all occurrences of "needle" with a quoted "replacement", * according to the allow_markup/plain_text settings. */ char *notification_replace_format(const char *needle, const char *replacement, char *haystack, bool allow_markup, bool plain_text) { char* tmp; char* ret; if (plain_text) { tmp = strdup(replacement); tmp = string_replace_all("\\n", "\n", tmp); if (settings.ignore_newline) { tmp = string_replace_all("\n", " ", tmp); } tmp = notification_quote_markup(tmp); ret = string_replace_all(needle, tmp, haystack); free(tmp); } else { tmp = strdup(replacement); if (settings.ignore_newline) { tmp = string_replace_all("
", " ", tmp); tmp = string_replace_all("
", " ", tmp); tmp = string_replace_all("
", " ", tmp); } else { tmp = string_replace_all("
", "\n", tmp); tmp = string_replace_all("
", "\n", tmp); tmp = string_replace_all("
", "\n", tmp); } if (!allow_markup) { tmp = notification_strip_markup(tmp); tmp = notification_quote_markup(tmp); } ret = string_replace_all(needle, tmp, haystack); free(tmp); } return ret; } char *notification_extract_markup_urls(char **str_ptr) { char *start, *end, *replace_buf, *str, *urls = NULL, *url, *index_buf; int linkno = 1; str = *str_ptr; while ((start = strstr(str, ""); if (end != NULL) { replace_buf = strndup(start, end - start + 1); url = extract_urls(replace_buf); if (url != NULL) { str = string_replace(replace_buf, "[", str); index_buf = g_strdup_printf("[#%d]", linkno++); if (urls == NULL) { urls = g_strconcat(index_buf, " ", url, NULL); } else { char *tmp = urls; urls = g_strconcat(tmp, "\n", index_buf, " ", url, NULL); free(tmp); } index_buf[0] = ' '; str = string_replace("", index_buf, str); free(index_buf); free(url); } else { str = string_replace(replace_buf, "", str); str = string_replace("", "", str); } free(replace_buf); } else { break; } } *str_ptr = str; return urls; } /* * Initialize the given notification and add it to * the queue. Replace notification with id if id > 0. */ int notification_init(notification * n, int id) { 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->text_to_render = NULL; n->format = settings.format; if (n->category == NULL) { n->category = ""; } rule_apply_all(n); n->urls = notification_extract_markup_urls(&(n->body)); n->msg = string_replace_all("\\n", "\n", g_strdup(n->format)); n->msg = notification_replace_format("%a", n->appname, n->msg, false, true); n->msg = notification_replace_format("%s", n->summary, n->msg, n->allow_markup, n->plain_text); n->msg = notification_replace_format("%b", n->body, n->msg, n->allow_markup, n->plain_text); if (n->icon) { n->msg = notification_replace_format("%I", basename(n->icon), n->msg, false, true); n->msg = notification_replace_format("%i", n->icon, n->msg, false, true); } if (n->progress) { char pg[10]; sprintf(pg, "[%3d%%]", n->progress - 1); n->msg = string_replace_all("%p", pg, n->msg); } else { n->msg = string_replace_all("%p", "", n->msg); } n->msg = g_strchomp(n->msg); if (id == 0) { n->id = ++next_notification_id; } else { notification_close_by_id(id, -1); n->id = id; } n->dup_count = 0; /* check if n is a duplicate */ if (settings.stack_duplicates) { 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->summary, n->summary) == 0 && strcmp(orig->body, n->body) == 0) { /* If the progress differs this was probably intended to replace the notification * but notify-send was used. So don't increment dup_count in this case */ if (orig->progress == n->progress) { orig->dup_count++; } else { orig->progress = n->progress; } /* notifications that differ only in progress hints should be expected equal, * but we want the latest message, with the latest hint value */ free(orig->msg); orig->msg = strdup(n->msg); 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->summary, n->summary) == 0 && strcmp(orig->body, n->body) == 0) { /* notifications that differ only in progress hints should be expected equal, * but we want the latest message, with the latest hint value */ free(orig->msg); orig->msg = strdup(n->msg); /* If the progress differs this was probably intended to replace the notification * but notify-send was used. So don't increment dup_count in this case */ if (orig->progress == n->progress) { orig->dup_count++; } else { orig->progress = n->progress; } 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]) { n->color_strings[ColFG] = xctx.color_strings[ColFG][n->urgency]; } if (!n->color_strings[ColBG]) { n->color_strings[ColBG] = xctx.color_strings[ColBG][n->urgency]; } if (!n->color_strings[ColFrame]) { n->color_strings[ColFrame] = xctx.color_strings[ColFrame][n->urgency]; } n->timeout = n->timeout == -1 ? settings.timeouts[n->urgency] : n->timeout; n->start = 0; if (n->icon != NULL && strlen(n->icon) <= 0) { free(n->icon); n->icon = NULL; } if (n->raw_icon == NULL && n->icon == NULL) { n->icon = strdup(settings.icons[n->urgency]); } n->timestamp = time(NULL); n->redisplayed = false; n->first_render = true; if (strlen(n->msg) == 0) { notification_close(n, 2); if (settings.always_run_script) { notification_run_script(n); } 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); char *tmp_urls = extract_urls(tmp); if (tmp_urls != NULL) { if (n->urls != NULL) { n->urls = string_append(n->urls, tmp_urls, "\n"); free(tmp_urls); } else { n->urls = tmp_urls; } } 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]; string_replace_char('[', '(', human_readable); // kill square brackets string_replace_char(']', ')', human_readable); char *act_str = g_strdup_printf("#%s [%s]", human_readable, n->appname); if (act_str) { n->actions->dmenu_str = string_append(n->actions->dmenu_str, act_str, "\n"); free(act_str); } } } 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); history_push(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); history_push(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); } void notification_update_text_to_render(notification *n) { if (n->text_to_render) { free(n->text_to_render); n->text_to_render = NULL; } char *buf = NULL; char *msg = g_strchomp(n->msg); /* print dup_count and msg */ if ((n->dup_count > 0 && !settings.hide_duplicates_count) && (n->actions || n->urls) && settings.show_indicators) { buf = g_strdup_printf("(%d%s%s) %s", n->dup_count, n->actions ? "A" : "", n->urls ? "U" : "", msg); } else if ((n->actions || n->urls) && settings.show_indicators) { buf = g_strdup_printf("(%s%s) %s", n->actions ? "A" : "", n->urls ? "U" : "", msg); } else if (n->dup_count > 0 && !settings.hide_duplicates_count) { buf = g_strdup_printf("(%d) %s", n->dup_count, 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; } n->text_to_render = buf; } int notification_get_ttl(notification *n) { if (n->timeout == 0) { return -1; } else { return n->timeout - (time(NULL) - n->start); } } int notification_get_age(notification *n) { return time(NULL) - n->timestamp; } /* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */