1 #define _POSIX_C_SOURCE 200809L
2 #include <arpa/inet.h>
3 #include <cairo.h>
4 #include <limits.h>
5 #include <stdbool.h>
6 #include <stdlib.h>
7 #include <string.h>
8 #include "swaybar/bar.h"
9 #include "swaybar/config.h"
10 #include "swaybar/input.h"
11 #include "swaybar/tray/host.h"
12 #include "swaybar/tray/icon.h"
13 #include "swaybar/tray/item.h"
14 #include "swaybar/tray/tray.h"
15 #include "background-image.h"
16 #include "cairo.h"
17 #include "list.h"
18 #include "log.h"
19 #include "wlr-layer-shell-unstable-v1-client-protocol.h"
20 
21 // TODO menu
22 
sni_ready(struct swaybar_sni * sni)23 static bool sni_ready(struct swaybar_sni *sni) {
24 	return sni->status && (sni->status[0] == 'N' ? // NeedsAttention
25 			sni->attention_icon_name || sni->attention_icon_pixmap :
26 			sni->icon_name || sni->icon_pixmap);
27 }
28 
set_sni_dirty(struct swaybar_sni * sni)29 static void set_sni_dirty(struct swaybar_sni *sni) {
30 	if (sni_ready(sni)) {
31 		sni->target_size = sni->min_size = sni->max_size = 0; // invalidate previous icon
32 		set_bar_dirty(sni->tray->bar);
33 	}
34 }
35 
read_pixmap(sd_bus_message * msg,struct swaybar_sni * sni,const char * prop,list_t ** dest)36 static int read_pixmap(sd_bus_message *msg, struct swaybar_sni *sni,
37 		const char *prop, list_t **dest) {
38 	int ret = sd_bus_message_enter_container(msg, 'a', "(iiay)");
39 	if (ret < 0) {
40 		sway_log(SWAY_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret));
41 		return ret;
42 	}
43 
44 	if (sd_bus_message_at_end(msg, 0)) {
45 		sway_log(SWAY_DEBUG, "%s %s no. of icons = 0", sni->watcher_id, prop);
46 		return ret;
47 	}
48 
49 	list_t *pixmaps = create_list();
50 	if (!pixmaps) {
51 		return -12; // -ENOMEM
52 	}
53 
54 	while (!sd_bus_message_at_end(msg, 0)) {
55 		ret = sd_bus_message_enter_container(msg, 'r', "iiay");
56 		if (ret < 0) {
57 			sway_log(SWAY_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret));
58 			goto error;
59 		}
60 
61 		int width, height;
62 		ret = sd_bus_message_read(msg, "ii", &width, &height);
63 		if (ret < 0) {
64 			sway_log(SWAY_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret));
65 			goto error;
66 		}
67 
68 		const void *pixels;
69 		size_t npixels;
70 		ret = sd_bus_message_read_array(msg, 'y', &pixels, &npixels);
71 		if (ret < 0) {
72 			sway_log(SWAY_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret));
73 			goto error;
74 		}
75 
76 		if (height > 0 && width == height) {
77 			sway_log(SWAY_DEBUG, "%s %s: found icon w:%d h:%d", sni->watcher_id, prop, width, height);
78 			struct swaybar_pixmap *pixmap =
79 				malloc(sizeof(struct swaybar_pixmap) + npixels);
80 			pixmap->size = height;
81 
82 			// convert from network byte order to host byte order
83 			for (int i = 0; i < height * width; ++i) {
84 				((uint32_t *)pixmap->pixels)[i] = ntohl(((uint32_t *)pixels)[i]);
85 			}
86 
87 			list_add(pixmaps, pixmap);
88 		} else {
89 			sway_log(SWAY_DEBUG, "%s %s: discard invalid icon w:%d h:%d", sni->watcher_id, prop, width, height);
90 		}
91 
92 		sd_bus_message_exit_container(msg);
93 	}
94 
95 	if (pixmaps->length < 1) {
96 		sway_log(SWAY_DEBUG, "%s %s no. of icons = 0", sni->watcher_id, prop);
97 		goto error;
98 	}
99 
100 	list_free_items_and_destroy(*dest);
101 	*dest = pixmaps;
102 	sway_log(SWAY_DEBUG, "%s %s no. of icons = %d", sni->watcher_id, prop,
103 			pixmaps->length);
104 
105 	return ret;
106 error:
107 	list_free_items_and_destroy(pixmaps);
108 	return ret;
109 }
110 
get_property_callback(sd_bus_message * msg,void * data,sd_bus_error * error)111 static int get_property_callback(sd_bus_message *msg, void *data,
112 		sd_bus_error *error) {
113 	struct swaybar_sni_slot *d = data;
114 	struct swaybar_sni *sni = d->sni;
115 	const char *prop = d->prop;
116 	const char *type = d->type;
117 	void *dest = d->dest;
118 
119 	int ret;
120 	if (sd_bus_message_is_method_error(msg, NULL)) {
121 		sway_log(SWAY_ERROR, "%s %s: %s", sni->watcher_id, prop,
122 				sd_bus_message_get_error(msg)->message);
123 		ret = sd_bus_message_get_errno(msg);
124 		goto cleanup;
125 	}
126 
127 	ret = sd_bus_message_enter_container(msg, 'v', type);
128 	if (ret < 0) {
129 		sway_log(SWAY_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret));
130 		goto cleanup;
131 	}
132 
133 	if (!type) {
134 		ret = read_pixmap(msg, sni, prop, dest);
135 		if (ret < 0) {
136 			goto cleanup;
137 		}
138 	} else {
139 		if (*type == 's' || *type == 'o') {
140 			free(*(char **)dest);
141 		}
142 
143 		ret = sd_bus_message_read(msg, type, dest);
144 		if (ret < 0) {
145 			sway_log(SWAY_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret));
146 			goto cleanup;
147 		}
148 
149 		if (*type == 's' || *type == 'o') {
150 			char **str = dest;
151 			*str = strdup(*str);
152 			sway_log(SWAY_DEBUG, "%s %s = '%s'", sni->watcher_id, prop, *str);
153 		} else if (*type == 'b') {
154 			sway_log(SWAY_DEBUG, "%s %s = %s", sni->watcher_id, prop,
155 					*(bool *)dest ? "true" : "false");
156 		}
157 	}
158 
159 	if (strcmp(prop, "Status") == 0 || (sni->status && (sni->status[0] == 'N' ?
160 				prop[0] == 'A' : strncmp(prop, "Icon", 4) == 0))) {
161 		set_sni_dirty(sni);
162 	}
163 cleanup:
164 	wl_list_remove(&d->link);
165 	free(data);
166 	return ret;
167 }
168 
sni_get_property_async(struct swaybar_sni * sni,const char * prop,const char * type,void * dest)169 static void sni_get_property_async(struct swaybar_sni *sni, const char *prop,
170 		const char *type, void *dest) {
171 	struct swaybar_sni_slot *data = calloc(1, sizeof(struct swaybar_sni_slot));
172 	data->sni = sni;
173 	data->prop = prop;
174 	data->type = type;
175 	data->dest = dest;
176 	int ret = sd_bus_call_method_async(sni->tray->bus, &data->slot, sni->service,
177 			sni->path, "org.freedesktop.DBus.Properties", "Get",
178 			get_property_callback, data, "ss", sni->interface, prop);
179 	if (ret >= 0) {
180 		wl_list_insert(&sni->slots, &data->link);
181 	} else {
182 		sway_log(SWAY_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret));
183 		free(data);
184 	}
185 }
186 
187 /*
188  * There is a quirk in sd-bus that in some systems, it is unable to get the
189  * well-known names on the bus, so it cannot identify if an incoming signal,
190  * which uses the sender's unique name, actually matches the callback's matching
191  * sender if the callback uses a well-known name, in which case it just calls
192  * the callback and hopes for the best, resulting in false positives. In the
193  * case of NewIcon & NewAttentionIcon, this doesn't affect anything, but it
194  * means that for NewStatus, if the SNI does not definitely match the sender,
195  * then the safe thing to do is to query the status independently.
196  * This function returns 1 if the SNI definitely matches the signal sender,
197  * which is returned by the calling function to indicate that signal matching
198  * can stop since it has already found the required callback, otherwise, it
199  * returns 0, which allows matching to continue.
200  */
sni_check_msg_sender(struct swaybar_sni * sni,sd_bus_message * msg,const char * signal)201 static int sni_check_msg_sender(struct swaybar_sni *sni, sd_bus_message *msg,
202 		const char *signal) {
203 	bool has_well_known_names =
204 		sd_bus_creds_get_mask(sd_bus_message_get_creds(msg)) & SD_BUS_CREDS_WELL_KNOWN_NAMES;
205 	if (sni->service[0] == ':' || has_well_known_names) {
206 		sway_log(SWAY_DEBUG, "%s has new %s", sni->watcher_id, signal);
207 		return 1;
208 	} else {
209 		sway_log(SWAY_DEBUG, "%s may have new %s", sni->watcher_id, signal);
210 		return 0;
211 	}
212 }
213 
handle_new_icon(sd_bus_message * msg,void * data,sd_bus_error * error)214 static int handle_new_icon(sd_bus_message *msg, void *data, sd_bus_error *error) {
215 	struct swaybar_sni *sni = data;
216 	sni_get_property_async(sni, "IconName", "s", &sni->icon_name);
217 	sni_get_property_async(sni, "IconPixmap", NULL, &sni->icon_pixmap);
218 	if (!strcmp(sni->interface, "org.kde.StatusNotifierItem")) {
219 		sni_get_property_async(sni, "IconThemePath", "s", &sni->icon_theme_path);
220 	}
221 	return sni_check_msg_sender(sni, msg, "icon");
222 }
223 
handle_new_attention_icon(sd_bus_message * msg,void * data,sd_bus_error * error)224 static int handle_new_attention_icon(sd_bus_message *msg, void *data,
225 		sd_bus_error *error) {
226 	struct swaybar_sni *sni = data;
227 	sni_get_property_async(sni, "AttentionIconName", "s", &sni->attention_icon_name);
228 	sni_get_property_async(sni, "AttentionIconPixmap", NULL, &sni->attention_icon_pixmap);
229 	return sni_check_msg_sender(sni, msg, "attention icon");
230 }
231 
handle_new_status(sd_bus_message * msg,void * data,sd_bus_error * error)232 static int handle_new_status(sd_bus_message *msg, void *data, sd_bus_error *error) {
233 	struct swaybar_sni *sni = data;
234 	int ret = sni_check_msg_sender(sni, msg, "status");
235 	if (ret == 1) {
236 		char *status;
237 		int r = sd_bus_message_read(msg, "s", &status);
238 		if (r < 0) {
239 			sway_log(SWAY_ERROR, "%s new status error: %s", sni->watcher_id, strerror(-ret));
240 			ret = r;
241 		} else {
242 			free(sni->status);
243 			sni->status = strdup(status);
244 			sway_log(SWAY_DEBUG, "%s has new status = '%s'", sni->watcher_id, status);
245 			set_sni_dirty(sni);
246 		}
247 	} else {
248 		sni_get_property_async(sni, "Status", "s", &sni->status);
249 	}
250 
251 	return ret;
252 }
253 
sni_match_signal_async(struct swaybar_sni * sni,char * signal,sd_bus_message_handler_t callback)254 static void sni_match_signal_async(struct swaybar_sni *sni, char *signal,
255 		sd_bus_message_handler_t callback) {
256 	struct swaybar_sni_slot *slot = calloc(1, sizeof(struct swaybar_sni_slot));
257 	int ret = sd_bus_match_signal_async(sni->tray->bus, &slot->slot,
258 			sni->service, sni->path, sni->interface, signal, callback, NULL, sni);
259 	if (ret >= 0) {
260 		wl_list_insert(&sni->slots, &slot->link);
261 	} else {
262 		sway_log(SWAY_ERROR, "%s failed to subscribe to signal %s: %s",
263 				sni->service, signal, strerror(-ret));
264 		free(slot);
265 	}
266 }
267 
create_sni(char * id,struct swaybar_tray * tray)268 struct swaybar_sni *create_sni(char *id, struct swaybar_tray *tray) {
269 	struct swaybar_sni *sni = calloc(1, sizeof(struct swaybar_sni));
270 	if (!sni) {
271 		return NULL;
272 	}
273 	sni->tray = tray;
274 	wl_list_init(&sni->slots);
275 	sni->watcher_id = strdup(id);
276 	char *path_ptr = strchr(id, '/');
277 	if (!path_ptr) {
278 		sni->service = strdup(id);
279 		sni->path = strdup("/StatusNotifierItem");
280 		sni->interface = "org.freedesktop.StatusNotifierItem";
281 	} else {
282 		sni->service = strndup(id, path_ptr - id);
283 		sni->path = strdup(path_ptr);
284 		sni->interface = "org.kde.StatusNotifierItem";
285 		sni_get_property_async(sni, "IconThemePath", "s", &sni->icon_theme_path);
286 	}
287 
288 	// Ignored: Category, Id, Title, WindowId, OverlayIconName,
289 	//          OverlayIconPixmap, AttentionMovieName, ToolTip
290 	sni_get_property_async(sni, "Status", "s", &sni->status);
291 	sni_get_property_async(sni, "IconName", "s", &sni->icon_name);
292 	sni_get_property_async(sni, "IconPixmap", NULL, &sni->icon_pixmap);
293 	sni_get_property_async(sni, "AttentionIconName", "s", &sni->attention_icon_name);
294 	sni_get_property_async(sni, "AttentionIconPixmap", NULL, &sni->attention_icon_pixmap);
295 	sni_get_property_async(sni, "ItemIsMenu", "b", &sni->item_is_menu);
296 	sni_get_property_async(sni, "Menu", "o", &sni->menu);
297 
298 	sni_match_signal_async(sni, "NewIcon", handle_new_icon);
299 	sni_match_signal_async(sni, "NewAttentionIcon", handle_new_attention_icon);
300 	sni_match_signal_async(sni, "NewStatus", handle_new_status);
301 
302 	return sni;
303 }
304 
destroy_sni(struct swaybar_sni * sni)305 void destroy_sni(struct swaybar_sni *sni) {
306 	if (!sni) {
307 		return;
308 	}
309 
310 	cairo_surface_destroy(sni->icon);
311 	free(sni->watcher_id);
312 	free(sni->service);
313 	free(sni->path);
314 	free(sni->status);
315 	free(sni->icon_name);
316 	list_free_items_and_destroy(sni->icon_pixmap);
317 	free(sni->attention_icon_name);
318 	list_free_items_and_destroy(sni->attention_icon_pixmap);
319 	free(sni->menu);
320 	free(sni->icon_theme_path);
321 
322 	struct swaybar_sni_slot *slot, *slot_tmp;
323 	wl_list_for_each_safe(slot, slot_tmp, &sni->slots, link) {
324 		sd_bus_slot_unref(slot->slot);
325 		free(slot);
326 	}
327 
328 	free(sni);
329 }
330 
handle_click(struct swaybar_sni * sni,int x,int y,uint32_t button,int delta)331 static void handle_click(struct swaybar_sni *sni, int x, int y,
332 		uint32_t button, int delta) {
333 	const char *method = NULL;
334 	struct tray_binding *binding = NULL;
335 	wl_list_for_each(binding, &sni->tray->bar->config->tray_bindings, link) {
336 		if (binding->button == button) {
337 			method = binding->command;
338 			break;
339 		}
340 	}
341 	if (!method) {
342 		static const char *default_bindings[10] = {
343 			"nop",
344 			"Activate",
345 			"SecondaryActivate",
346 			"ContextMenu",
347 			"ScrollUp",
348 			"ScrollDown",
349 			"ScrollLeft",
350 			"ScrollRight",
351 			"nop",
352 			"nop"
353 		};
354 		method = default_bindings[event_to_x11_button(button)];
355 	}
356 	if (strcmp(method, "nop") == 0) {
357 		return;
358 	}
359 	if (sni->item_is_menu && strcmp(method, "Activate") == 0) {
360 		method = "ContextMenu";
361 	}
362 
363 	if (strncmp(method, "Scroll", strlen("Scroll")) == 0) {
364 		char dir = method[strlen("Scroll")];
365 		char *orientation = (dir == 'U' || dir == 'D') ? "vertical" : "horizontal";
366 		int sign = (dir == 'U' || dir == 'L') ? -1 : 1;
367 
368 		sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, sni->path,
369 				sni->interface, "Scroll", NULL, NULL, "is", delta*sign, orientation);
370 	} else {
371 		sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, sni->path,
372 				sni->interface, method, NULL, NULL, "ii", x, y);
373 	}
374 }
375 
cmp_sni_id(const void * item,const void * cmp_to)376 static int cmp_sni_id(const void *item, const void *cmp_to) {
377 	const struct swaybar_sni *sni = item;
378 	return strcmp(sni->watcher_id, cmp_to);
379 }
380 
icon_hotspot_callback(struct swaybar_output * output,struct swaybar_hotspot * hotspot,double x,double y,uint32_t button,void * data)381 static enum hotspot_event_handling icon_hotspot_callback(
382 		struct swaybar_output *output, struct swaybar_hotspot *hotspot,
383 		double x, double y, uint32_t button, void *data) {
384 	sway_log(SWAY_DEBUG, "Clicked on %s", (char *)data);
385 
386 	struct swaybar_tray *tray = output->bar->tray;
387 	int idx = list_seq_find(tray->items, cmp_sni_id, data);
388 
389 	if (idx != -1) {
390 		struct swaybar_sni *sni = tray->items->items[idx];
391 		// guess global position since wayland doesn't expose it
392 		struct swaybar_config *config = tray->bar->config;
393 		int global_x = output->output_x + config->gaps.left + x;
394 		bool top_bar = config->position & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP;
395 		int global_y = output->output_y + (top_bar ? config->gaps.top + y:
396 				(int) output->output_height - config->gaps.bottom - y);
397 
398 		sway_log(SWAY_DEBUG, "Guessing click position at (%d, %d)", global_x, global_y);
399 		handle_click(sni, global_x, global_y, button, 1); // TODO get delta from event
400 		return HOTSPOT_IGNORE;
401 	} else {
402 		sway_log(SWAY_DEBUG, "but it doesn't exist");
403 	}
404 
405 	return HOTSPOT_PROCESS;
406 }
407 
reload_sni(struct swaybar_sni * sni,char * icon_theme,int target_size)408 static void reload_sni(struct swaybar_sni *sni, char *icon_theme,
409 		int target_size) {
410 	char *icon_name = sni->status[0] == 'N' ?
411 		sni->attention_icon_name : sni->icon_name;
412 	if (icon_name) {
413 		list_t *icon_search_paths = create_list();
414 		list_cat(icon_search_paths, sni->tray->basedirs);
415 		if (sni->icon_theme_path) {
416 			list_add(icon_search_paths, sni->icon_theme_path);
417 		}
418 		char *icon_path = find_icon(sni->tray->themes, icon_search_paths,
419 				icon_name, target_size, icon_theme,
420 				&sni->min_size, &sni->max_size);
421 		list_free(icon_search_paths);
422 		if (icon_path) {
423 			cairo_surface_destroy(sni->icon);
424 			sni->icon = load_background_image(icon_path);
425 			free(icon_path);
426 			return;
427 		}
428 	}
429 
430 	list_t *pixmaps = sni->status[0] == 'N' ?
431 		sni->attention_icon_pixmap : sni->icon_pixmap;
432 	if (pixmaps) {
433 		struct swaybar_pixmap *pixmap = NULL;
434 		int min_error = INT_MAX;
435 		for (int i = 0; i < pixmaps->length; ++i) {
436 			struct swaybar_pixmap *p = pixmaps->items[i];
437 			int e = abs(target_size - p->size);
438 			if (e < min_error) {
439 				pixmap = p;
440 				min_error = e;
441 			}
442 		}
443 		cairo_surface_destroy(sni->icon);
444 		sni->icon = cairo_image_surface_create_for_data(pixmap->pixels,
445 				CAIRO_FORMAT_ARGB32, pixmap->size, pixmap->size,
446 				cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, pixmap->size));
447 	}
448 }
449 
render_sni(cairo_t * cairo,struct swaybar_output * output,double * x,struct swaybar_sni * sni)450 uint32_t render_sni(cairo_t *cairo, struct swaybar_output *output, double *x,
451 		struct swaybar_sni *sni) {
452 	uint32_t height = output->height * output->scale;
453 	int padding = output->bar->config->tray_padding;
454 	int target_size = height - 2*padding;
455 	if (target_size != sni->target_size && sni_ready(sni)) {
456 		// check if another icon should be loaded
457 		if (target_size < sni->min_size || target_size > sni->max_size) {
458 			reload_sni(sni, output->bar->config->icon_theme, target_size);
459 		}
460 
461 		sni->target_size = target_size;
462 	}
463 
464 	int icon_size;
465 	cairo_surface_t *icon;
466 	if (sni->icon) {
467 		int actual_size = cairo_image_surface_get_height(sni->icon);
468 		icon_size = actual_size < target_size ?
469 			actual_size*(target_size/actual_size) : target_size;
470 		icon = cairo_image_surface_scale(sni->icon, icon_size, icon_size);
471 	} else { // draw a :(
472 		icon_size = target_size*0.8;
473 		icon = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, icon_size, icon_size);
474 		cairo_t *cairo_icon = cairo_create(icon);
475 		cairo_set_source_u32(cairo_icon, 0xFF0000FF);
476 		cairo_translate(cairo_icon, icon_size/2, icon_size/2);
477 		cairo_scale(cairo_icon, icon_size/2, icon_size/2);
478 		cairo_arc(cairo_icon, 0, 0, 1, 0, 7);
479 		cairo_fill(cairo_icon);
480 		cairo_set_operator(cairo_icon, CAIRO_OPERATOR_CLEAR);
481 		cairo_arc(cairo_icon, 0.35, -0.3, 0.1, 0, 7);
482 		cairo_fill(cairo_icon);
483 		cairo_arc(cairo_icon, -0.35, -0.3, 0.1, 0, 7);
484 		cairo_fill(cairo_icon);
485 		cairo_arc(cairo_icon, 0, 0.75, 0.5, 3.71238898038469, 5.71238898038469);
486 		cairo_set_line_width(cairo_icon, 0.1);
487 		cairo_stroke(cairo_icon);
488 		cairo_destroy(cairo_icon);
489 	}
490 
491 	int padded_size = icon_size + 2*padding;
492 	*x -= padded_size;
493 	int y = floor((height - padded_size) / 2.0);
494 
495 	cairo_operator_t op = cairo_get_operator(cairo);
496 	cairo_set_operator(cairo, CAIRO_OPERATOR_OVER);
497 	cairo_set_source_surface(cairo, icon, *x + padding, y + padding);
498 	cairo_rectangle(cairo, *x, y, padded_size, padded_size);
499 	cairo_fill(cairo);
500 	cairo_set_operator(cairo, op);
501 
502 	cairo_surface_destroy(icon);
503 
504 	struct swaybar_hotspot *hotspot = calloc(1, sizeof(struct swaybar_hotspot));
505 	hotspot->x = *x;
506 	hotspot->y = 0;
507 	hotspot->width = height;
508 	hotspot->height = height;
509 	hotspot->callback = icon_hotspot_callback;
510 	hotspot->destroy = free;
511 	hotspot->data = strdup(sni->watcher_id);
512 	wl_list_insert(&output->hotspots, &hotspot->link);
513 
514 	return output->height;
515 }
516