/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ /** * @file src/queues.c * @brief All important functions to handle the notification queues for * history, entrance and currently displayed ones. * * Every method requires to have executed queues_init() at the start. * * A read only representation of the queue with the current notifications * can get acquired by calling queues_get_displayed(). * * When ending the program or resetting the queues, tear down the stack with * queues_teardown(). (And reinit with queues_init() if needed.) */ #include "queues.h" #include #include #include #include #include "dunst.h" #include "log.h" #include "notification.h" #include "settings.h" #include "utils.h" /* notification lists */ static GQueue *waiting = NULL; /**< all new notifications get into here */ static GQueue *displayed = NULL; /**< currently displayed notifications */ static GQueue *history = NULL; /**< history of displayed notifications */ int next_notification_id = 1; static bool queues_stack_duplicate(struct notification *n); static bool queues_stack_by_tag(struct notification *n); /* see queues.h */ void queues_init(void) { history = g_queue_new(); displayed = g_queue_new(); waiting = g_queue_new(); } /* see queues.h */ const GList *queues_get_displayed(void) { return g_queue_peek_head_link(displayed); } /* see queues.h */ const struct notification *queues_get_head_waiting(void) { if (waiting->length == 0) return NULL; return g_queue_peek_head(waiting); } /* see queues.h */ unsigned int queues_length_waiting(void) { return waiting->length; } /* see queues.h */ unsigned int queues_length_displayed(void) { return displayed->length; } /* see queues.h */ unsigned int queues_length_history(void) { return history->length; } /** * Swap two given queue elements. The element's data has to be a notification. * * @pre { elemA has to be part of queueA. } * @pre { elemB has to be part of queueB. } * * @param queueA The queue, which elemB's data will get inserted * @param elemA The element, which will get removed from queueA * @param queueB The queue, which elemA's data will get inserted * @param elemB The element, which will get removed from queueB */ static void queues_swap_notifications(GQueue *queueA, GList *elemA, GQueue *queueB, GList *elemB) { struct notification *toB = elemA->data; struct notification *toA = elemB->data; g_queue_delete_link(queueA, elemA); g_queue_delete_link(queueB, elemB); if (toA) g_queue_insert_sorted(queueA, toA, notification_cmp_data, NULL); if (toB) g_queue_insert_sorted(queueB, toB, notification_cmp_data, NULL); } /** * Check if a notification is eligible to get shown. * * @param n The notification to check * @param status The current status of dunst * @param shown True if the notification is currently displayed */ static bool queues_notification_is_ready(const struct notification *n, struct dunst_status status, bool shown) { ASSERT_OR_RET(status.running, false); if (status.fullscreen && shown) return n && n->fullscreen != FS_PUSHBACK; else if (status.fullscreen && !shown) return n && n->fullscreen == FS_SHOW; else return true; } /** * Check if a notification has timed out * * @param n the notification to check * @param status the current status of dunst * @retval true: the notification is timed out * @retval false: otherwise */ static bool queues_notification_is_finished(struct notification *n, struct dunst_status status) { assert(n); if (n->skip_display && !n->redisplayed) return true; if (n->timeout == 0) // sticky return false; bool is_idle = status.fullscreen ? false : status.idle; /* don't timeout when user is idle */ if (is_idle && !n->transient) { n->start = time_monotonic_now(); return false; } /* remove old message */ if (time_monotonic_now() - n->start > n->timeout) { return true; } return false; } /* see queues.h */ int queues_notification_insert(struct notification *n) { /* do not display the message, if the message is empty */ if (STR_EMPTY(n->msg)) { if (settings.always_run_script) { notification_run_script(n); } LOG_M("Skipping notification: '%s' '%s'", n->body, n->summary); return 0; } /* Do not insert the message if it's a command */ if (STR_EQ("DUNST_COMMAND_PAUSE", n->summary)) { dunst_status(S_RUNNING, false); return 0; } if (STR_EQ("DUNST_COMMAND_RESUME", n->summary)) { dunst_status(S_RUNNING, true); return 0; } if (STR_EQ("DUNST_COMMAND_TOGGLE", n->summary)) { dunst_status(S_RUNNING, !dunst_status_get().running); return 0; } bool inserted = false; if (n->id != 0) { if (!queues_notification_replace_id(n)) { // Requested id was not valid, but play nice and assign it anyway g_queue_insert_sorted(waiting, n, notification_cmp_data, NULL); } inserted = true; } else { n->id = ++next_notification_id; } if (!inserted && STR_FULL(n->stack_tag) && queues_stack_by_tag(n)) inserted = true; if (!inserted && settings.stack_duplicates && queues_stack_duplicate(n)) inserted = true; if (!inserted) g_queue_insert_sorted(waiting, n, notification_cmp_data, NULL); if (settings.print_notifications) notification_print(n); return n->id; } /** * Replaces duplicate notification and stacks it * * @retval true: notification got stacked * @retval false: notification did not get stacked */ static bool queues_stack_duplicate(struct notification *n) { GQueue *allqueues[] = { displayed, waiting }; for (int i = 0; i < sizeof(allqueues)/sizeof(GQueue*); i++) { for (GList *iter = g_queue_peek_head_link(allqueues[i]); iter; iter = iter->next) { struct notification *orig = iter->data; if (notification_is_duplicate(orig, n)) { /* If the progress differs, probably notify-send was used to update the notification * So only count it as a duplicate, if the progress was not the same. * */ if (orig->progress == n->progress) { orig->dup_count++; } else { orig->progress = n->progress; } iter->data = n; n->dup_count = orig->dup_count; signal_notification_closed(orig, 1); if (allqueues[i] == displayed) n->start = time_monotonic_now(); notification_unref(orig); return true; } } } return false; } /** * Replaces the first notification of the same stack_tag * * @retval true: notification got stacked * @retval false: notification did not get stacked */ static bool queues_stack_by_tag(struct notification *new) { GQueue *allqueues[] = { displayed, waiting }; for (int i = 0; i < sizeof(allqueues)/sizeof(GQueue*); i++) { for (GList *iter = g_queue_peek_head_link(allqueues[i]); iter; iter = iter->next) { struct notification *old = iter->data; if (STR_FULL(old->stack_tag) && STR_EQ(old->stack_tag, new->stack_tag)) { iter->data = new; new->dup_count = old->dup_count; signal_notification_closed(old, 1); if (allqueues[i] == displayed) { new->start = time_monotonic_now(); notification_run_script(new); } notification_unref(old); return true; } } } return false; } /* see queues.h */ bool queues_notification_replace_id(struct notification *new) { GQueue *allqueues[] = { displayed, waiting }; for (int i = 0; i < sizeof(allqueues)/sizeof(GQueue*); i++) { for (GList *iter = g_queue_peek_head_link(allqueues[i]); iter; iter = iter->next) { struct notification *old = iter->data; if (old->id == new->id) { iter->data = new; new->dup_count = old->dup_count; if (allqueues[i] == displayed) { new->start = time_monotonic_now(); notification_run_script(new); } notification_unref(old); return true; } } } return false; } /* see queues.h */ void queues_notification_close_id(int id, enum reason reason) { struct notification *target = NULL; GQueue *allqueues[] = { displayed, waiting }; for (int i = 0; i < sizeof(allqueues)/sizeof(GQueue*); i++) { for (GList *iter = g_queue_peek_head_link(allqueues[i]); iter; iter = iter->next) { struct notification *n = iter->data; if (n->id == id) { g_queue_remove(allqueues[i], n); target = n; break; } } } if (target) { //Don't notify clients if notification was pulled from history if (!target->redisplayed) signal_notification_closed(target, reason); queues_history_push(target); } } /* see queues.h */ void queues_notification_close(struct notification *n, enum reason reason) { assert(n != NULL); queues_notification_close_id(n->id, reason); } /* see queues.h */ void queues_history_pop(void) { if (g_queue_is_empty(history)) return; struct notification *n = g_queue_pop_tail(history); n->redisplayed = true; n->timeout = settings.sticky_history ? 0 : n->timeout; g_queue_insert_sorted(waiting, n, notification_cmp_data, NULL); } /* see queues.h */ void queues_history_push(struct notification *n) { if (!n->history_ignore) { if (settings.history_length > 0 && history->length >= settings.history_length) { struct notification *to_free = g_queue_pop_head(history); notification_unref(to_free); } g_queue_push_tail(history, n); } else { notification_unref(n); } } /* see queues.h */ void queues_history_push_all(void) { while (displayed->length > 0) { queues_notification_close(g_queue_peek_head_link(displayed)->data, REASON_USER); } while (waiting->length > 0) { queues_notification_close(g_queue_peek_head_link(waiting)->data, REASON_USER); } } /* see queues.h */ void queues_update(struct dunst_status status) { GList *iter, *nextiter; /* Move back all notifications, which aren't eligible to get shown anymore * Will move the notifications back to waiting, if dunst isn't running or fullscreen * and notifications is not eligible to get shown anymore */ iter = g_queue_peek_head_link(displayed); while (iter) { struct notification *n = iter->data; nextiter = iter->next; if (queues_notification_is_finished(n, status)){ queues_notification_close(n, REASON_TIME); iter = nextiter; continue; } if (!queues_notification_is_ready(n, status, true)) { g_queue_delete_link(displayed, iter); g_queue_insert_sorted(waiting, n, notification_cmp_data, NULL); iter = nextiter; continue; } iter = nextiter; } int cur_displayed_limit; if (settings.geometry.h == 0) cur_displayed_limit = INT_MAX; else if ( settings.indicate_hidden && settings.geometry.h > 1 && displayed->length + waiting->length > settings.geometry.h) cur_displayed_limit = settings.geometry.h-1; else cur_displayed_limit = settings.geometry.h; /* move notifications from queue to displayed */ iter = g_queue_peek_head_link(waiting); while (displayed->length < cur_displayed_limit && iter) { struct notification *n = iter->data; nextiter = iter->next; ASSERT_OR_RET(n,); if (!queues_notification_is_ready(n, status, false)) { iter = nextiter; continue; } n->start = time_monotonic_now(); notification_run_script(n); if (n->skip_display && !n->redisplayed) { queues_notification_close(n, REASON_USER); } else { g_queue_delete_link(waiting, iter); g_queue_insert_sorted(displayed, n, notification_cmp_data, NULL); } iter = nextiter; } /* if necessary, push the overhanging notifications from displayed to waiting again */ while (displayed->length > cur_displayed_limit) { struct notification *n = g_queue_pop_tail(displayed); g_queue_insert_sorted(waiting, n, notification_cmp_data, NULL); //TODO: actually it should be on the head if unsorted } /* If displayed is actually full, let the more important notifications * from waiting seep into displayed. */ if (settings.sort && displayed->length == cur_displayed_limit) { GList *i_waiting, *i_displayed; while ( (i_waiting = g_queue_peek_head_link(waiting)) && (i_displayed = g_queue_peek_tail_link(displayed))) { while (i_waiting && ! queues_notification_is_ready(i_waiting->data, status, false)) { i_waiting = i_waiting->prev; } if (i_waiting && notification_cmp(i_displayed->data, i_waiting->data) > 0) { struct notification *todisp = i_waiting->data; todisp->start = time_monotonic_now(); notification_run_script(todisp); queues_swap_notifications(displayed, i_displayed, waiting, i_waiting); } else { break; } } } } /* see queues.h */ gint64 queues_get_next_datachange(gint64 time) { gint64 sleep = G_MAXINT64; for (GList *iter = g_queue_peek_head_link(displayed); iter; iter = iter->next) { struct notification *n = iter->data; gint64 ttl = n->timeout - (time - n->start); if (n->timeout > 0) { if (ttl > 0) sleep = MIN(sleep, ttl); else // while we're processing, the notification already timed out return 0; } if (settings.show_age_threshold >= 0) { gint64 age = time - n->timestamp; // sleep exactly until the next shift of the second happens if (age > settings.show_age_threshold - S2US(1)) sleep = MIN(sleep, (S2US(1) - (age % S2US(1)))); else sleep = MIN(sleep, settings.show_age_threshold - age); } } return sleep != G_MAXINT64 ? sleep : -1; } /** * Helper function for queues_teardown() to free a single notification * * @param data The notification to free */ static void teardown_notification(gpointer data) { struct notification *n = data; notification_unref(n); } /* see queues.h */ void queues_teardown(void) { g_queue_free_full(history, teardown_notification); history = NULL; g_queue_free_full(displayed, teardown_notification); displayed = NULL; g_queue_free_full(waiting, teardown_notification); waiting = NULL; } /* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */