1 #define _POSIX_C_SOURCE 200809L
2 #include <assert.h>
3 #include <ctype.h>
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <sys/wait.h>
8 #include <unistd.h>
9
10 #include <pango/pangocairo.h>
11 #include <wayland-client.h>
12 #include <linux/input-event-codes.h>
13
14 #include "config.h"
15 #include "criteria.h"
16 #include "dbus.h"
17 #include "event-loop.h"
18 #include "mako.h"
19 #include "notification.h"
20 #include "icon.h"
21 #include "string-util.h"
22 #include "wayland.h"
23
hotspot_at(struct mako_hotspot * hotspot,int32_t x,int32_t y)24 bool hotspot_at(struct mako_hotspot *hotspot, int32_t x, int32_t y) {
25 return x >= hotspot->x &&
26 y >= hotspot->y &&
27 x < hotspot->x + hotspot->width &&
28 y < hotspot->y + hotspot->height;
29 }
30
reset_notification(struct mako_notification * notif)31 void reset_notification(struct mako_notification *notif) {
32 struct mako_action *action, *tmp;
33 wl_list_for_each_safe(action, tmp, ¬if->actions, link) {
34 wl_list_remove(&action->link);
35 free(action->key);
36 free(action->title);
37 free(action);
38 }
39
40 notif->urgency = MAKO_NOTIFICATION_URGENCY_UNKNOWN;
41 notif->progress = -1;
42
43 destroy_timer(notif->timer);
44 notif->timer = NULL;
45
46 free(notif->app_name);
47 free(notif->app_icon);
48 free(notif->summary);
49 free(notif->body);
50 free(notif->category);
51 free(notif->desktop_entry);
52 free(notif->tag);
53 if (notif->image_data != NULL) {
54 free(notif->image_data->data);
55 free(notif->image_data);
56 }
57
58 notif->app_name = strdup("");
59 notif->app_icon = strdup("");
60 notif->summary = strdup("");
61 notif->body = strdup("");
62 notif->category = strdup("");
63 notif->desktop_entry = strdup("");
64 notif->tag = strdup("");
65
66 notif->image_data = NULL;
67
68 destroy_icon(notif->icon);
69 notif->icon = NULL;
70 }
71
create_notification(struct mako_state * state)72 struct mako_notification *create_notification(struct mako_state *state) {
73 struct mako_notification *notif =
74 calloc(1, sizeof(struct mako_notification));
75 if (notif == NULL) {
76 fprintf(stderr, "allocation failed\n");
77 return NULL;
78 }
79
80 notif->state = state;
81 ++state->last_id;
82 notif->id = state->last_id;
83 wl_list_init(¬if->actions);
84 wl_list_init(¬if->link);
85 reset_notification(notif);
86
87 // Start ungrouped.
88 notif->group_index = -1;
89
90 return notif;
91 }
92
destroy_notification(struct mako_notification * notif)93 void destroy_notification(struct mako_notification *notif) {
94 wl_list_remove(¬if->link);
95
96 reset_notification(notif);
97 finish_style(¬if->style);
98 free(notif);
99 }
100
close_notification(struct mako_notification * notif,enum mako_notification_close_reason reason)101 void close_notification(struct mako_notification *notif,
102 enum mako_notification_close_reason reason) {
103 notify_notification_closed(notif, reason);
104 wl_list_remove(¬if->link); // Remove so regrouping works...
105 wl_list_init(¬if->link); // ...but destroy will remove again.
106
107 struct mako_criteria *notif_criteria = create_criteria_from_notification(
108 notif, ¬if->style.group_criteria_spec);
109 if (notif_criteria) {
110 group_notifications(notif->state, notif_criteria);
111 destroy_criteria(notif_criteria);
112 }
113
114 if (!notif->style.history ||
115 notif->state->config.max_history <= 0) {
116 destroy_notification(notif);
117 return;
118 }
119
120 destroy_timer(notif->timer);
121 notif->timer = NULL;
122
123 wl_list_insert(¬if->state->history, ¬if->link);
124 while (wl_list_length(¬if->state->history) >
125 notif->state->config.max_history) {
126 struct mako_notification *n =
127 wl_container_of(notif->state->history.prev, n, link);
128 destroy_notification(n);
129 }
130 }
131
get_notification(struct mako_state * state,uint32_t id)132 struct mako_notification *get_notification(struct mako_state *state,
133 uint32_t id) {
134 struct mako_notification *notif;
135 wl_list_for_each(notif, &state->notifications, link) {
136 if (notif->id == id) {
137 return notif;
138 }
139 }
140 return NULL;
141 }
142
get_tagged_notification(struct mako_state * state,const char * tag,const char * app_name)143 struct mako_notification *get_tagged_notification(struct mako_state *state,
144 const char *tag, const char *app_name) {
145 struct mako_notification *notif;
146 wl_list_for_each(notif, &state->notifications, link) {
147 if (notif->tag && strlen(notif->tag) != 0 &&
148 strcmp(notif->tag, tag) == 0 &&
149 strcmp(notif->app_name, app_name) == 0) {
150 return notif;
151 }
152 }
153 return NULL;
154 }
155
close_group_notifications(struct mako_notification * top_notif,enum mako_notification_close_reason reason)156 void close_group_notifications(struct mako_notification *top_notif,
157 enum mako_notification_close_reason reason) {
158 struct mako_state *state = top_notif->state;
159
160 if (top_notif->style.group_criteria_spec.none) {
161 // No grouping, just close the notification
162 close_notification(top_notif, reason);
163 return;
164 }
165
166 struct mako_criteria *notif_criteria = create_criteria_from_notification(
167 top_notif, &top_notif->style.group_criteria_spec);
168
169 struct mako_notification *notif, *tmp;
170 wl_list_for_each_safe(notif, tmp, &state->notifications, link) {
171 if (match_criteria(notif_criteria, notif)) {
172 close_notification(notif, reason);
173 }
174 }
175
176 destroy_criteria(notif_criteria);
177 }
178
close_all_notifications(struct mako_state * state,enum mako_notification_close_reason reason)179 void close_all_notifications(struct mako_state *state,
180 enum mako_notification_close_reason reason) {
181 struct mako_notification *notif, *tmp;
182 wl_list_for_each_safe(notif, tmp, &state->notifications, link) {
183 close_notification(notif, reason);
184 }
185 }
186
trim_space(char * dst,const char * src)187 static size_t trim_space(char *dst, const char *src) {
188 size_t src_len = strlen(src);
189 const char *start = src;
190 const char *end = src + src_len;
191
192 while (start != end && isspace(start[0])) {
193 ++start;
194 }
195
196 while (end != start && isspace(end[-1])) {
197 --end;
198 }
199
200 size_t trimmed_len = end - start;
201 memmove(dst, start, trimmed_len);
202 dst[trimmed_len] = '\0';
203 return trimmed_len;
204 }
205
escape_markup_char(char c)206 static const char *escape_markup_char(char c) {
207 switch (c) {
208 case '&': return "&";
209 case '<': return "<";
210 case '>': return ">";
211 case '\'': return "'";
212 case '"': return """;
213 }
214 return NULL;
215 }
216
escape_markup(const char * s,char * buf)217 static size_t escape_markup(const char *s, char *buf) {
218 size_t len = 0;
219 while (s[0] != '\0') {
220 const char *replacement = escape_markup_char(s[0]);
221 if (replacement != NULL) {
222 size_t replacement_len = strlen(replacement);
223 if (buf != NULL) {
224 memcpy(buf + len, replacement, replacement_len);
225 }
226 len += replacement_len;
227 } else {
228 if (buf != NULL) {
229 buf[len] = s[0];
230 }
231 ++len;
232 }
233 ++s;
234 }
235 if (buf != NULL) {
236 buf[len] = '\0';
237 }
238 return len;
239 }
240
241 // Any new format specifiers must also be added to VALID_FORMAT_SPECIFIERS.
242
format_hidden_text(char variable,bool * markup,void * data)243 char *format_hidden_text(char variable, bool *markup, void *data) {
244 struct mako_hidden_format_data *format_data = data;
245 switch (variable) {
246 case 'h':
247 return mako_asprintf("%zu", format_data->hidden);
248 case 't':
249 return mako_asprintf("%zu", format_data->count);
250 }
251 return NULL;
252 }
253
format_notif_text(char variable,bool * markup,void * data)254 char *format_notif_text(char variable, bool *markup, void *data) {
255 struct mako_notification *notif = data;
256 switch (variable) {
257 case 'a':
258 return strdup(notif->app_name);
259 case 's':
260 return strdup(notif->summary);
261 case 'b':
262 *markup = notif->style.markup;
263 return strdup(notif->body);
264 case 'g':
265 return mako_asprintf("%d", notif->group_count);
266 }
267 return NULL;
268 }
269
format_text(const char * format,char * buf,mako_format_func_t format_func,void * data)270 size_t format_text(const char *format, char *buf, mako_format_func_t format_func, void *data) {
271 size_t len = 0;
272
273 const char *last = format;
274 while (1) {
275 char *current = strchr(last, '%');
276 if (current == NULL || current[1] == '\0') {
277 size_t tail_len = strlen(last);
278 if (buf != NULL) {
279 memcpy(buf + len, last, tail_len + 1);
280 }
281 len += tail_len;
282 break;
283 }
284
285 size_t chunk_len = current - last;
286 if (buf != NULL) {
287 memcpy(buf + len, last, chunk_len);
288 }
289 len += chunk_len;
290
291 char *value = NULL;
292 bool markup = false;
293
294 if (current[1] == '%') {
295 value = strdup("%");
296 } else {
297 value = format_func(current[1], &markup, data);
298 }
299 if (value == NULL) {
300 value = strdup("");
301 }
302
303 size_t value_len;
304 if (!markup || !pango_parse_markup(value, -1, 0, NULL, NULL, NULL, NULL)) {
305 char *escaped = NULL;
306 if (buf != NULL) {
307 escaped = buf + len;
308 }
309 value_len = escape_markup(value, escaped);
310 } else {
311 value_len = strlen(value);
312 if (buf != NULL) {
313 memcpy(buf + len, value, value_len);
314 }
315 }
316 free(value);
317
318 len += value_len;
319 last = current + 2;
320 }
321
322 if (buf != NULL) {
323 trim_space(buf, buf);
324 }
325 return len;
326 }
327
get_button_binding(struct mako_style * style,uint32_t button)328 static const struct mako_binding *get_button_binding(struct mako_style *style,
329 uint32_t button) {
330 switch (button) {
331 case BTN_LEFT:
332 return &style->button_bindings.left;
333 case BTN_RIGHT:
334 return &style->button_bindings.right;
335 case BTN_MIDDLE:
336 return &style->button_bindings.middle;
337 }
338 return NULL;
339 }
340
notification_execute_binding(struct mako_notification * notif,const struct mako_binding * binding,const struct mako_binding_context * ctx)341 void notification_execute_binding(struct mako_notification *notif,
342 const struct mako_binding *binding,
343 const struct mako_binding_context *ctx) {
344 switch (binding->action) {
345 case MAKO_BINDING_NONE:
346 break;
347 case MAKO_BINDING_DISMISS:
348 close_notification(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED);
349 break;
350 case MAKO_BINDING_DISMISS_GROUP:
351 close_group_notifications(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED);
352 break;
353 case MAKO_BINDING_DISMISS_ALL:
354 close_all_notifications(notif->state, MAKO_NOTIFICATION_CLOSE_DISMISSED);
355 break;
356 case MAKO_BINDING_INVOKE_DEFAULT_ACTION:;
357 struct mako_action *action;
358 wl_list_for_each(action, ¬if->actions, link) {
359 if (strcmp(action->key, DEFAULT_ACTION_KEY) == 0) {
360 char *activation_token = NULL;
361 if (ctx != NULL) {
362 activation_token = create_xdg_activation_token(ctx->surface,
363 ctx->seat, ctx->serial);
364 }
365 notify_action_invoked(action, activation_token);
366 free(activation_token);
367 break;
368 }
369 }
370 close_notification(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED);
371 break;
372 case MAKO_BINDING_EXEC:
373 assert(binding->command != NULL);
374 pid_t pid = fork();
375 if (pid < 0) {
376 perror("fork failed");
377 break;
378 } else if (pid == 0) {
379 // Double-fork to avoid SIGCHLD issues
380 pid = fork();
381 if (pid < 0) {
382 perror("fork failed");
383 _exit(1);
384 } else if (pid == 0) {
385 // We pass variables using additional sh arguments. To convert
386 // back the arguments to variables, insert a short script
387 // preamble before the user's command.
388 const char setup_vars[] = "id=\"$1\"\n";
389
390 size_t cmd_size = strlen(setup_vars) + strlen(binding->command) + 1;
391 char *cmd = malloc(cmd_size);
392 snprintf(cmd, cmd_size, "%s%s", setup_vars, binding->command);
393
394 char id_str[32];
395 snprintf(id_str, sizeof(id_str), "%" PRIu32, notif->id);
396
397 char *const argv[] = { "sh", "-c", cmd, "sh", id_str, NULL };
398 execvp("sh", argv);
399 perror("exec failed");
400 _exit(1);
401 }
402 _exit(0);
403 }
404 if (waitpid(pid, NULL, 0) < 0) {
405 perror("waitpid failed");
406 }
407 break;
408 }
409 }
410
notification_handle_button(struct mako_notification * notif,uint32_t button,enum wl_pointer_button_state state,const struct mako_binding_context * ctx)411 void notification_handle_button(struct mako_notification *notif, uint32_t button,
412 enum wl_pointer_button_state state,
413 const struct mako_binding_context *ctx) {
414 if (state != WL_POINTER_BUTTON_STATE_PRESSED) {
415 return;
416 }
417
418 const struct mako_binding *binding =
419 get_button_binding(¬if->style, button);
420 if (binding != NULL) {
421 notification_execute_binding(notif, binding, ctx);
422 }
423 }
424
notification_handle_touch(struct mako_notification * notif,const struct mako_binding_context * ctx)425 void notification_handle_touch(struct mako_notification *notif,
426 const struct mako_binding_context *ctx) {
427 notification_execute_binding(notif, ¬if->style.touch_binding, ctx);
428 }
429
430 /*
431 * Searches through the notifications list and returns the next position at
432 * which to insert. If no results for the specified urgency are found,
433 * it will return the closest link searching in the direction specified.
434 * (-1 for lower, 1 or upper).
435 */
get_last_notif_by_urgency(struct wl_list * notifications,enum mako_notification_urgency urgency,int direction)436 static struct wl_list *get_last_notif_by_urgency(struct wl_list *notifications,
437 enum mako_notification_urgency urgency, int direction) {
438 enum mako_notification_urgency current = urgency;
439
440 if (wl_list_empty(notifications)) {
441 return notifications;
442 }
443
444 while (current <= MAKO_NOTIFICATION_URGENCY_CRITICAL &&
445 current >= MAKO_NOTIFICATION_URGENCY_UNKNOWN) {
446 struct mako_notification *notif;
447 wl_list_for_each_reverse(notif, notifications, link) {
448 if (notif->urgency == current) {
449 return ¬if->link;
450 }
451 }
452 current += direction;
453 }
454
455 return notifications;
456 }
457
insert_notification(struct mako_state * state,struct mako_notification * notif)458 void insert_notification(struct mako_state *state, struct mako_notification *notif) {
459 struct mako_config *config = &state->config;
460 struct wl_list *insert_node;
461
462 if (config->sort_criteria == MAKO_SORT_CRITERIA_TIME &&
463 !(config->sort_asc & MAKO_SORT_CRITERIA_TIME)) {
464 insert_node = &state->notifications;
465 } else if (config->sort_criteria == MAKO_SORT_CRITERIA_TIME &&
466 (config->sort_asc & MAKO_SORT_CRITERIA_TIME)) {
467 insert_node = state->notifications.prev;
468 } else if (config->sort_criteria & MAKO_SORT_CRITERIA_URGENCY) {
469 int direction = (config->sort_asc & MAKO_SORT_CRITERIA_URGENCY) ? -1 : 1;
470 int offset = 0;
471 if (!(config->sort_asc & MAKO_SORT_CRITERIA_TIME)) {
472 offset = direction;
473 }
474 insert_node = get_last_notif_by_urgency(&state->notifications,
475 notif->urgency + offset, direction);
476 } else {
477 insert_node = &state->notifications;
478 }
479
480 wl_list_insert(insert_node, ¬if->link);
481 }
482
483 // Iterate through all of the current notifications and group any that share
484 // the same values for all of the criteria fields in `spec`. Returns the number
485 // of notifications in the resulting group, or -1 if something goes wrong
486 // with criteria.
group_notifications(struct mako_state * state,struct mako_criteria * criteria)487 int group_notifications(struct mako_state *state, struct mako_criteria *criteria) {
488 struct wl_list matches = {0};
489 wl_list_init(&matches);
490
491 // Now we're going to find all of the matching notifications and stick
492 // them in a different list. Removing the first one from the global list
493 // is technically unnecessary, since it will go back in the same place, but
494 // it makes the rest of this logic nicer.
495 struct wl_list *location = NULL; // The place we're going to reinsert them.
496 struct mako_notification *notif = NULL, *tmp = NULL;
497 size_t count = 0;
498 wl_list_for_each_safe(notif, tmp, &state->notifications, link) {
499 if (!match_criteria(criteria, notif)) {
500 continue;
501 }
502
503 if (!location) {
504 location = notif->link.prev;
505 }
506
507 wl_list_remove(¬if->link);
508 wl_list_insert(matches.prev, ¬if->link);
509 notif->group_index = count++;
510 }
511
512 // If count is zero, we don't need to worry about changing anything. The
513 // notification's style has its grouping criteria set to none.
514
515 if (count == 1) {
516 // If we matched a single notification, it means that it has grouping
517 // criteria set, but didn't have any others to group with. This makes
518 // it ungrouped just as if it had no grouping criteria. If this is a
519 // new notification, its index is already set to -1. However, this also
520 // happens when a notification had been part of a group and all the
521 // others have closed, so we need to set it anyway.
522 // We can't use the current pointer, wl_list_for_each_safe clobbers it.
523 notif = wl_container_of(matches.prev, notif, link);
524 notif->group_index = -1;
525 }
526
527 // Now we need to rematch criteria for all of the grouped notifications,
528 // in case it changes their styles. We also take this opportunity to record
529 // the total number of notifications in the group, so that it can be used
530 // in the notifications' format.
531 // We can't skip this even if there was only a single match, as we may be
532 // removing the second-to-last notification of a group, and still need to
533 // potentially change style now that the matched one isn't in a group
534 // anymore.
535 wl_list_for_each(notif, &matches, link) {
536 notif->group_count = count;
537 }
538
539 // Place all of the matches back into the list where the first one was
540 // originally.
541 wl_list_insert_list(location, &matches);
542
543 // We don't actually re-apply criteria here, that will happen just before
544 // we render each notification anyway.
545
546 return count;
547 }
548