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, &notif->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(&notif->actions);
84 	wl_list_init(&notif->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(&notif->link);
95 
96 	reset_notification(notif);
97 	finish_style(&notif->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(&notif->link);  // Remove so regrouping works...
105 	wl_list_init(&notif->link);  // ...but destroy will remove again.
106 
107 	struct mako_criteria *notif_criteria = create_criteria_from_notification(
108 			notif, &notif->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(&notif->state->history, &notif->link);
124 	while (wl_list_length(&notif->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 "&amp;";
209 	case '<': return "&lt;";
210 	case '>': return "&gt;";
211 	case '\'': return "&apos;";
212 	case '"': return "&quot;";
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, &notif->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(&notif->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, &notif->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 &notif->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, &notif->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(&notif->link);
508 		wl_list_insert(matches.prev, &notif->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