From cf228147250f4616d150fbe5276088c5f9969bba Mon Sep 17 00:00:00 2001
From: vetu104 <vetu104@proton.me>
Date: Sat, 29 Mar 2025 19:22:37 +0200
Subject: [PATCH] Add a system tray next to sewn's bar

---
 Makefile          |  23 +-
 config.def.h      |   5 +
 dbus.c            | 242 +++++++++++++++
 dbus.h            |  10 +
 dwl.c             | 107 ++++++-
 systray/helpers.c |  43 +++
 systray/helpers.h |  12 +
 systray/icon.c    | 149 +++++++++
 systray/icon.h    |  26 ++
 systray/item.c    | 403 ++++++++++++++++++++++++
 systray/item.h    |  46 +++
 systray/menu.c    | 757 ++++++++++++++++++++++++++++++++++++++++++++++
 systray/menu.h    |  11 +
 systray/tray.c    | 237 +++++++++++++++
 systray/tray.h    |  37 +++
 systray/watcher.c | 551 +++++++++++++++++++++++++++++++++
 systray/watcher.h |  35 +++
 17 files changed, 2681 insertions(+), 13 deletions(-)
 create mode 100644 dbus.c
 create mode 100644 dbus.h
 create mode 100644 systray/helpers.c
 create mode 100644 systray/helpers.h
 create mode 100644 systray/icon.c
 create mode 100644 systray/icon.h
 create mode 100644 systray/item.c
 create mode 100644 systray/item.h
 create mode 100644 systray/menu.c
 create mode 100644 systray/menu.h
 create mode 100644 systray/tray.c
 create mode 100644 systray/tray.h
 create mode 100644 systray/watcher.c
 create mode 100644 systray/watcher.h

diff --git a/Makefile b/Makefile
index 9bc67db..9d50189 100644
--- a/Makefile
+++ b/Makefile
@@ -12,17 +12,28 @@ DWLDEVCFLAGS = -g -pedantic -Wall -Wextra -Wdeclaration-after-statement \
 	-Wfloat-conversion
 
 # CFLAGS / LDFLAGS
-PKGS      = wlroots-0.18 wayland-server xkbcommon libinput pixman-1 fcft $(XLIBS)
+PKGS      = wlroots-0.18 wayland-server xkbcommon libinput pixman-1 fcft $(XLIBS) dbus-1
 DWLCFLAGS = `$(PKG_CONFIG) --cflags $(PKGS)` $(DWLCPPFLAGS) $(DWLDEVCFLAGS) $(CFLAGS)
 LDLIBS    = `$(PKG_CONFIG) --libs $(PKGS)` -lm $(LIBS)
 
+TRAYOBJS = systray/watcher.o systray/tray.o systray/item.o systray/icon.o systray/menu.o systray/helpers.o
+TRAYDEPS = systray/watcher.h systray/tray.h systray/item.h systray/icon.h systray/menu.h systray/helpers.h
+
 all: dwl
-dwl: dwl.o util.o
-	$(CC) dwl.o util.o $(DWLCFLAGS) $(LDFLAGS) $(LDLIBS) -o $@
-dwl.o: dwl.c client.h config.h config.mk cursor-shape-v1-protocol.h \
+dwl: dwl.o util.o dbus.o $(TRAYOBJS) $(TRAYDEPS)
+	$(CC) dwl.o util.o dbus.o $(TRAYOBJS) $(DWLCFLAGS) $(LDFLAGS) $(LDLIBS) -o $@
+dwl.o: dwl.c client.h dbus.h config.h config.mk cursor-shape-v1-protocol.h \
 	pointer-constraints-unstable-v1-protocol.h wlr-layer-shell-unstable-v1-protocol.h \
-	wlr-output-power-management-unstable-v1-protocol.h xdg-shell-protocol.h
+	wlr-output-power-management-unstable-v1-protocol.h xdg-shell-protocol.h \
+	$(TRAYDEPS)
 util.o: util.c util.h
+dbus.o: dbus.c dbus.h
+systray/watcher.o: systray/watcher.c $(TRAYDEPS)
+systray/tray.o: systray/tray.c $(TRAYDEPS)
+systray/item.o: systray/item.c $(TRAYDEPS)
+systray/icon.o: systray/icon.c $(TRAYDEPS)
+systray/menu.o: systray/menu.c $(TRAYDEPS)
+systray/helpers.o: systray/helpers.c $(TRAYDEPS)
 
 # wayland-scanner is a tool which generates C headers and rigging for Wayland
 # protocols, which are specified in XML. wlroots requires you to rig these up
@@ -49,7 +60,7 @@ xdg-shell-protocol.h:
 config.h:
 	cp config.def.h $@
 clean:
-	rm -f dwl *.o *-protocol.h
+	rm -f dwl *.o *-protocol.h systray/*.o
 
 dist: clean
 	mkdir -p dwl-$(VERSION)
diff --git a/config.def.h b/config.def.h
index 5d1dc2b..451643e 100644
--- a/config.def.h
+++ b/config.def.h
@@ -7,6 +7,8 @@
 static const int sloppyfocus               = 1;  /* focus follows mouse */
 static const int bypass_surface_visibility = 0;  /* 1 means idle inhibitors will disable idle tracking even if it's surface isn't visible  */
 static const unsigned int borderpx         = 1;  /* border pixel of windows */
+static const unsigned int systrayspacing   = 2; /* systray spacing */
+static const int showsystray               = 1; /* 0 means no systray */
 static const int showbar                   = 1; /* 0 means no bar */
 static const int topbar                    = 1; /* 0 means bottom bar */
 static const char *fonts[]                 = {"monospace:size=10"};
@@ -127,6 +129,7 @@ static const enum libinput_config_tap_button_map button_map = LIBINPUT_CONFIG_TA
 /* commands */
 static const char *termcmd[] = { "foot", NULL };
 static const char *menucmd[] = { "wmenu-run", NULL };
+static const char *dmenucmd[] = { "wmenu", NULL };
 
 static const Key keys[] = {
 	/* Note that Shift changes certain key codes: c -> C, 2 -> at, etc. */
@@ -188,4 +191,6 @@ static const Button buttons[] = {
 	{ ClkTagBar,   0,      BTN_RIGHT,  toggleview,     {0} },
 	{ ClkTagBar,   MODKEY, BTN_LEFT,   tag,            {0} },
 	{ ClkTagBar,   MODKEY, BTN_RIGHT,  toggletag,      {0} },
+	{ ClkTray,     0,      BTN_LEFT,   trayactivate,   {0} },
+	{ ClkTray,     0,      BTN_RIGHT,  traymenu,       {0} },
 };
diff --git a/dbus.c b/dbus.c
new file mode 100644
index 0000000..125312c
--- /dev/null
+++ b/dbus.c
@@ -0,0 +1,242 @@
+#include "dbus.h"
+
+#include "util.h"
+
+#include <dbus/dbus.h>
+#include <stdlib.h>
+#include <wayland-server-core.h>
+
+#include <fcntl.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <unistd.h>
+
+static void
+close_pipe(void *data)
+{
+	int *pipefd = data;
+
+	close(pipefd[0]);
+	close(pipefd[1]);
+	free(pipefd);
+}
+
+static int
+dwl_dbus_dispatch(int fd, unsigned int mask, void *data)
+{
+	DBusConnection *conn = data;
+
+	int pending;
+	DBusDispatchStatus oldstatus, newstatus;
+
+	oldstatus = dbus_connection_get_dispatch_status(conn);
+	newstatus = dbus_connection_dispatch(conn);
+
+	/* Don't clear pending flag if status didn't change */
+	if (oldstatus == newstatus)
+		return 0;
+
+	if (read(fd, &pending, sizeof(int)) < 0) {
+		perror("read");
+		die("Error in dbus dispatch");
+	}
+
+	return 0;
+}
+
+static int
+dwl_dbus_watch_handle(int fd, uint32_t mask, void *data)
+{
+	DBusWatch *watch = data;
+
+	uint32_t flags = 0;
+
+	if (!dbus_watch_get_enabled(watch))
+		return 0;
+
+	if (mask & WL_EVENT_READABLE)
+		flags |= DBUS_WATCH_READABLE;
+	if (mask & WL_EVENT_WRITABLE)
+		flags |= DBUS_WATCH_WRITABLE;
+	if (mask & WL_EVENT_HANGUP)
+		flags |= DBUS_WATCH_HANGUP;
+	if (mask & WL_EVENT_ERROR)
+		flags |= DBUS_WATCH_ERROR;
+
+	dbus_watch_handle(watch, flags);
+
+	return 0;
+}
+
+static dbus_bool_t
+dwl_dbus_add_watch(DBusWatch *watch, void *data)
+{
+	struct wl_event_loop *loop = data;
+
+	int fd;
+	struct wl_event_source *watch_source;
+	uint32_t mask = 0, flags;
+
+	if (!dbus_watch_get_enabled(watch))
+		return TRUE;
+
+	flags = dbus_watch_get_flags(watch);
+	if (flags & DBUS_WATCH_READABLE)
+		mask |= WL_EVENT_READABLE;
+	if (flags & DBUS_WATCH_WRITABLE)
+		mask |= WL_EVENT_WRITABLE;
+
+	fd = dbus_watch_get_unix_fd(watch);
+	watch_source = wl_event_loop_add_fd(loop, fd, mask,
+	                                    dwl_dbus_watch_handle, watch);
+
+	dbus_watch_set_data(watch, watch_source, NULL);
+
+	return TRUE;
+}
+
+static void
+dwl_dbus_remove_watch(DBusWatch *watch, void *data)
+{
+	struct wl_event_source *watch_source = dbus_watch_get_data(watch);
+
+	if (watch_source)
+		wl_event_source_remove(watch_source);
+}
+
+static int
+dwl_dbus_timeout_handle(void *data)
+{
+	DBusTimeout *timeout = data;
+
+	if (dbus_timeout_get_enabled(timeout))
+		dbus_timeout_handle(timeout);
+
+	return 0;
+}
+
+static dbus_bool_t
+dwl_dbus_add_timeout(DBusTimeout *timeout, void *data)
+{
+	struct wl_event_loop *loop = data;
+
+	int r, interval;
+	struct wl_event_source *timeout_source;
+
+	if (!dbus_timeout_get_enabled(timeout))
+		return TRUE;
+
+	interval = dbus_timeout_get_interval(timeout);
+
+	timeout_source =
+		wl_event_loop_add_timer(loop, dwl_dbus_timeout_handle, timeout);
+
+	r = wl_event_source_timer_update(timeout_source, interval);
+	if (r < 0) {
+		wl_event_source_remove(timeout_source);
+		return FALSE;
+	}
+
+	dbus_timeout_set_data(timeout, timeout_source, NULL);
+
+	return TRUE;
+}
+
+static void
+dwl_dbus_remove_timeout(DBusTimeout *timeout, void *data)
+{
+	struct wl_event_source *timeout_source;
+
+	timeout_source = dbus_timeout_get_data(timeout);
+
+	if (timeout_source) {
+		wl_event_source_timer_update(timeout_source, 0);
+		wl_event_source_remove(timeout_source);
+	}
+}
+
+static void
+dwl_dbus_dispatch_status(DBusConnection *conn, DBusDispatchStatus status,
+                         void *data)
+{
+	int *pipefd = data;
+
+	if (status != DBUS_DISPATCH_COMPLETE) {
+		int pending = 1;
+		if (write(pipefd[1], &pending, sizeof(int)) < 0) {
+			perror("write");
+			die("Error in dispatch status");
+		}
+	}
+}
+
+struct wl_event_source *
+startbus(DBusConnection *conn, struct wl_event_loop *loop)
+{
+	int *pipefd;
+	int pending = 1, flags;
+	struct wl_event_source *bus_source = NULL;
+
+	pipefd = ecalloc(2, sizeof(int));
+
+	/*
+	 * Libdbus forbids calling dbus_connection_dipatch from the
+	 * DBusDispatchStatusFunction directly. Notify the event loop of
+	 * updates via a self-pipe.
+	 */
+	if (pipe(pipefd) < 0)
+		goto fail;
+	if (((flags = fcntl(pipefd[0], F_GETFD)) < 0) ||
+	    fcntl(pipefd[0], F_SETFD, flags | FD_CLOEXEC) < 0 ||
+	    ((flags = fcntl(pipefd[1], F_GETFD)) < 0) ||
+	    fcntl(pipefd[1], F_SETFD, flags | FD_CLOEXEC) < 0) {
+		goto fail;
+	}
+
+	dbus_connection_set_exit_on_disconnect(conn, FALSE);
+
+	bus_source = wl_event_loop_add_fd(loop, pipefd[0], WL_EVENT_READABLE,
+	                                  dwl_dbus_dispatch, conn);
+	if (!bus_source)
+		goto fail;
+
+	dbus_connection_set_dispatch_status_function(conn,
+	                                             dwl_dbus_dispatch_status,
+	                                             pipefd, close_pipe);
+	if (!dbus_connection_set_watch_functions(conn, dwl_dbus_add_watch,
+	                                         dwl_dbus_remove_watch, NULL,
+	                                         loop, NULL)) {
+		goto fail;
+	}
+	if (!dbus_connection_set_timeout_functions(conn, dwl_dbus_add_timeout,
+	                                           dwl_dbus_remove_timeout,
+	                                           NULL, loop, NULL)) {
+		goto fail;
+	}
+	if (dbus_connection_get_dispatch_status(conn) != DBUS_DISPATCH_COMPLETE)
+		if (write(pipefd[1], &pending, sizeof(int)) < 0)
+			goto fail;
+
+	return bus_source;
+
+fail:
+	if (bus_source)
+		wl_event_source_remove(bus_source);
+	dbus_connection_set_timeout_functions(conn, NULL, NULL, NULL, NULL,
+	                                      NULL);
+	dbus_connection_set_watch_functions(conn, NULL, NULL, NULL, NULL, NULL);
+	dbus_connection_set_dispatch_status_function(conn, NULL, NULL, NULL);
+
+	return NULL;
+}
+
+void
+stopbus(DBusConnection *conn, struct wl_event_source *bus_source)
+{
+	wl_event_source_remove(bus_source);
+	dbus_connection_set_watch_functions(conn, NULL, NULL, NULL, NULL, NULL);
+	dbus_connection_set_timeout_functions(conn, NULL, NULL, NULL, NULL,
+	                                      NULL);
+	dbus_connection_set_dispatch_status_function(conn, NULL, NULL, NULL);
+}
diff --git a/dbus.h b/dbus.h
new file mode 100644
index 0000000..b374b98
--- /dev/null
+++ b/dbus.h
@@ -0,0 +1,10 @@
+#ifndef DWLDBUS_H
+#define DWLDBUS_H
+
+#include <dbus/dbus.h>
+#include <wayland-server-core.h>
+
+struct wl_event_source* startbus (DBusConnection *conn, struct wl_event_loop *loop);
+void stopbus (DBusConnection *conn, struct wl_event_source *bus_source);
+
+#endif /* DWLDBUS_H */
diff --git a/dwl.c b/dwl.c
index ece537a..7753ef6 100644
--- a/dwl.c
+++ b/dwl.c
@@ -1,6 +1,7 @@
 /*
  * See LICENSE file for copyright and license details.
  */
+#include <dbus/dbus.h>
 #include <getopt.h>
 #include <libinput.h>
 #include <linux/input-event-codes.h>
@@ -71,6 +72,9 @@
 
 #include "util.h"
 #include "drwl.h"
+#include "dbus.h"
+#include "systray/tray.h"
+#include "systray/watcher.h"
 
 /* macros */
 #define MAX(A, B)               ((A) > (B) ? (A) : (B))
@@ -89,7 +93,7 @@ enum { SchemeNorm, SchemeSel, SchemeUrg }; /* color schemes */
 enum { CurNormal, CurPressed, CurMove, CurResize }; /* cursor */
 enum { XDGShell, LayerShell, X11 }; /* client types */
 enum { LyrBg, LyrBottom, LyrTile, LyrFloat, LyrTop, LyrFS, LyrOverlay, LyrBlock, NUM_LAYERS }; /* scene layers */
-enum { ClkTagBar, ClkLtSymbol, ClkStatus, ClkTitle, ClkClient, ClkRoot }; /* clicks */
+enum { ClkTagBar, ClkLtSymbol, ClkStatus, ClkTitle, ClkClient, ClkRoot, ClkTray }; /* clicks */
 #ifdef XWAYLAND
 enum { NetWMWindowTypeDialog, NetWMWindowTypeSplash, NetWMWindowTypeToolbar,
 	NetWMWindowTypeUtility, NetLast }; /* EWMH atoms */
@@ -218,6 +222,7 @@ struct Monitor {
 		int real_width, real_height; /* non-scaled */
 		float scale;
 	} b; /* bar area */
+	Tray *tray;
 	struct wlr_box w; /* window area, layout-relative */
 	struct wl_list layers[4]; /* LayerSurface.link */
 	const Layout *lt[2];
@@ -376,6 +381,9 @@ static void togglefloating(const Arg *arg);
 static void togglefullscreen(const Arg *arg);
 static void toggletag(const Arg *arg);
 static void toggleview(const Arg *arg);
+static void trayactivate(const Arg *arg);
+static void traymenu(const Arg *arg);
+static void traynotify(void *data);
 static void unlocksession(struct wl_listener *listener, void *data);
 static void unmaplayersurfacenotify(struct wl_listener *listener, void *data);
 static void unmapnotify(struct wl_listener *listener, void *data);
@@ -451,6 +459,10 @@ static Monitor *selmon;
 static char stext[256];
 static struct wl_event_source *status_event_source;
 
+static DBusConnection *bus_conn;
+static struct wl_event_source *bus_source;
+static Watcher watcher = {.running = 0};
+
 static const struct wlr_buffer_impl buffer_impl = {
     .destroy = bufdestroy,
     .begin_data_ptr_access = bufdatabegin,
@@ -721,8 +733,8 @@ bufrelease(struct wl_listener *listener, void *data)
 void
 buttonpress(struct wl_listener *listener, void *data)
 {
-	unsigned int i = 0, x = 0;
-	double cx;
+	unsigned int i = 0, x = 0, ti = 0;
+	double cx, tx = 0;
 	unsigned int click;
 	struct wlr_pointer_button_event *event = data;
 	struct wlr_keyboard *keyboard;
@@ -732,6 +744,7 @@ buttonpress(struct wl_listener *listener, void *data)
 	Arg arg = {0};
 	Client *c;
 	const Button *b;
+	int traywidth;
 
 	wlr_idle_notifier_v1_notify_activity(idle_notifier, seat);
 
@@ -751,6 +764,8 @@ buttonpress(struct wl_listener *listener, void *data)
 			(node = wlr_scene_node_at(&layers[LyrBottom]->node, cursor->x, cursor->y, NULL, NULL)) &&
 			(buffer = wlr_scene_buffer_from_node(node)) && buffer == selmon->scene_buffer) {
 			cx = (cursor->x - selmon->m.x) * selmon->wlr_output->scale;
+			traywidth = tray_get_width(selmon->tray);
+
 			do
 				x += TEXTW(selmon, tags[i]);
 			while (cx >= x && ++i < LENGTH(tags));
@@ -759,8 +774,16 @@ buttonpress(struct wl_listener *listener, void *data)
 				arg.ui = 1 << i;
 			} else if (cx < x + TEXTW(selmon, selmon->ltsymbol))
 				click = ClkLtSymbol;
-			else if (cx > selmon->b.width - (TEXTW(selmon, stext) - selmon->lrpad + 2)) {
+			else if (cx > selmon->b.width - (TEXTW(selmon, stext) - selmon->lrpad + 2) && cx < selmon->b.width - traywidth) {
 				click = ClkStatus;
+			} else if (cx > selmon->b.width - (TEXTW(selmon, stext) - selmon->lrpad + 2)) {
+				unsigned int tray_n_items = watcher_get_n_items(&watcher);
+				tx = selmon->b.width - traywidth;
+				do
+					tx += tray_n_items ? (int)(traywidth / tray_n_items) : 0;
+				while (cx >= tx && ++ti < tray_n_items);
+				click = ClkTray;
+				arg.ui = ti;
 			} else
 				click = ClkTitle;
 		}
@@ -774,7 +797,12 @@ buttonpress(struct wl_listener *listener, void *data)
 		mods = keyboard ? wlr_keyboard_get_modifiers(keyboard) : 0;
 		for (b = buttons; b < END(buttons); b++) {
 			if (CLEANMASK(mods) == CLEANMASK(b->mod) && event->button == b->button && click == b->click && b->func) {
-				b->func(click == ClkTagBar && b->arg.i == 0 ? &arg : &b->arg);
+				if (click == ClkTagBar && b->arg.i == 0)
+					b->func(&arg);
+				else if (click == ClkTray && b->arg.i == 0)
+					b->func(&arg);
+				else
+					b->func(&b->arg);
 				return;
 			}
 		}
@@ -840,6 +868,14 @@ cleanup(void)
 
 	destroykeyboardgroup(&kb_group->destroy, NULL);
 
+	if (watcher.running)
+		watcher_stop(&watcher);
+
+	if (showbar && showsystray) {
+		stopbus(bus_conn, bus_source);
+		dbus_connection_unref(bus_conn);
+	}
+
 	/* If it's not destroyed manually it will cause a use-after-free of wlr_seat.
 	 * Destroy it until it's fixed in the wlroots side */
 	wlr_backend_destroy(backend);
@@ -868,6 +904,9 @@ cleanupmon(struct wl_listener *listener, void *data)
 	for (i = 0; i < LENGTH(m->pool); i++)
 		wlr_buffer_drop(&m->pool[i]->base);
 
+	if (showsystray)
+		destroytray(m->tray);
+
 	drwl_setimage(m->drw, NULL);
 	drwl_destroy(m->drw);
 
@@ -1506,6 +1545,7 @@ dirtomon(enum wlr_direction dir)
 void
 drawbar(Monitor *m)
 {
+	int traywidth = 0;
 	int x, w, tw = 0;
 	int boxs = m->drw->font->height / 9;
 	int boxw = m->drw->font->height / 6 + 2;
@@ -1518,11 +1558,13 @@ drawbar(Monitor *m)
 	if (!(buf = bufmon(m)))
 		return;
 
+	traywidth = tray_get_width(m->tray);
+
 	/* draw status first so it can be overdrawn by tags later */
 	if (m == selmon) { /* status is only drawn on selected monitor */
 		drwl_setscheme(m->drw, colors[SchemeNorm]);
 		tw = TEXTW(m, stext) - m->lrpad + 2; /* 2px right padding */
-		drwl_text(m->drw, m->b.width - tw, 0, tw, m->b.height, 0, stext, 0);
+		drwl_text(m->drw, m->b.width - (tw + traywidth), 0, tw, m->b.height, 0, stext, 0);
 	}
 
 	wl_list_for_each(c, &clients, link) {
@@ -1548,7 +1590,7 @@ drawbar(Monitor *m)
 	drwl_setscheme(m->drw, colors[SchemeNorm]);
 	x = drwl_text(m->drw, x, 0, w, m->b.height, m->lrpad / 2, m->ltsymbol, 0);
 
-	if ((w = m->b.width - tw - x) > m->b.height) {
+	if ((w = m->b.width - (tw + x + traywidth)) > m->b.height) {
 		if (c) {
 			drwl_setscheme(m->drw, colors[m == selmon ? SchemeSel : SchemeNorm]);
 			drwl_text(m->drw, x, 0, w, m->b.height, m->lrpad / 2, client_get_title(c), 0);
@@ -1560,6 +1602,15 @@ drawbar(Monitor *m)
 		}
 	}
 
+	if (traywidth > 0) {
+		pixman_image_composite32(PIXMAN_OP_SRC,
+		                         m->tray->image, NULL, m->drw->image,
+		                         0, 0,
+		                         0, 0,
+		                         m->b.width - traywidth, 0,
+		                         traywidth, m->b.height);
+	}
+
 	wlr_scene_buffer_set_dest_size(m->scene_buffer,
 		m->b.real_width, m->b.real_height);
 	wlr_scene_node_set_position(&m->scene_buffer->node, m->m.x,
@@ -1568,6 +1619,26 @@ drawbar(Monitor *m)
 	wlr_buffer_unlock(&buf->base);
 }
 
+void
+traynotify(void *data)
+{
+	Monitor *m = data;
+
+	drawbar(m);
+}
+
+void
+trayactivate(const Arg *arg)
+{
+	tray_leftclicked(selmon->tray, arg->ui);
+}
+
+void
+traymenu(const Arg *arg)
+{
+	tray_rightclicked(selmon->tray, arg->ui, dmenucmd);
+}
+
 void
 drawbars(void)
 {
@@ -2818,6 +2889,15 @@ setup(void)
 	status_event_source = wl_event_loop_add_fd(wl_display_get_event_loop(dpy),
 		STDIN_FILENO, WL_EVENT_READABLE, statusin, NULL);
 
+	bus_conn = dbus_bus_get(DBUS_BUS_SESSION, NULL);
+	if (!bus_conn)
+		die("Failed to connect to bus");
+	bus_source = startbus(bus_conn, event_loop);
+	if (!bus_source)
+		die("Failed to start listening to bus events");
+	if (showbar && showsystray)
+		watcher_start(&watcher, bus_conn, event_loop);
+
 	/* Make sure XWayland clients don't connect to the parent X server,
 	 * e.g when running in the x11 backend or the wayland backend and the
 	 * compositor has Xwayland support */
@@ -3160,6 +3240,7 @@ updatebar(Monitor *m)
 	size_t i;
 	int rw, rh;
 	char fontattrs[12];
+	Tray *tray;
 
 	wlr_output_transformed_resolution(m->wlr_output, &rw, &rh);
 	m->b.width = rw;
@@ -3185,6 +3266,18 @@ updatebar(Monitor *m)
 	m->lrpad = m->drw->font->height;
 	m->b.height = m->drw->font->height + 2;
 	m->b.real_height = (int)((float)m->b.height / m->wlr_output->scale);
+
+	if (showsystray) {
+		if (m->tray)
+			destroytray(m->tray);
+		tray = createtray(m,
+		                  m->b.height, systrayspacing, colors[SchemeNorm], fonts, fontattrs,
+		                  &traynotify, &watcher);
+		if (!tray)
+			die("Couldn't create tray for monitor");
+		m->tray = tray;
+		wl_list_insert(&watcher.trays, &tray->link);
+	}
 }
 
 void
diff --git a/systray/helpers.c b/systray/helpers.c
new file mode 100644
index 0000000..d1af9f8
--- /dev/null
+++ b/systray/helpers.c
@@ -0,0 +1,43 @@
+#include "helpers.h"
+
+#include <dbus/dbus.h>
+
+#include <errno.h>
+#include <stddef.h>
+
+// IWYU pragma: no_include "dbus/dbus-protocol.h"
+// IWYU pragma: no_include "dbus/dbus-shared.h"
+
+int
+request_property(DBusConnection *conn, const char *busname, const char *busobj,
+                 const char *prop, const char *iface, PropHandler handler,
+                 void *data)
+{
+	DBusMessage *msg = NULL;
+	DBusPendingCall *pending = NULL;
+	int r;
+
+	if (!(msg = dbus_message_new_method_call(busname, busobj,
+	                                         DBUS_INTERFACE_PROPERTIES,
+	                                         "Get")) ||
+	    !dbus_message_append_args(msg, DBUS_TYPE_STRING, &iface,
+	                              DBUS_TYPE_STRING, &prop,
+	                              DBUS_TYPE_INVALID) ||
+	    !dbus_connection_send_with_reply(conn, msg, &pending, -1) ||
+	    !dbus_pending_call_set_notify(pending, handler, data, NULL)) {
+		r = -ENOMEM;
+		goto fail;
+	}
+
+	dbus_message_unref(msg);
+	return 0;
+
+fail:
+	if (pending) {
+		dbus_pending_call_cancel(pending);
+		dbus_pending_call_unref(pending);
+	}
+	if (msg)
+		dbus_message_unref(msg);
+	return r;
+}
diff --git a/systray/helpers.h b/systray/helpers.h
new file mode 100644
index 0000000..2c592e0
--- /dev/null
+++ b/systray/helpers.h
@@ -0,0 +1,12 @@
+#ifndef HELPERS_H
+#define HELPERS_H
+
+#include <dbus/dbus.h>
+
+typedef void (*PropHandler)(DBusPendingCall *pcall, void *data);
+
+int request_property (DBusConnection *conn, const char *busname,
+                      const char *busobj, const char *prop, const char *iface,
+                      PropHandler handler, void *data);
+
+#endif /* HELPERS_H */
diff --git a/systray/icon.c b/systray/icon.c
new file mode 100644
index 0000000..1b97866
--- /dev/null
+++ b/systray/icon.c
@@ -0,0 +1,149 @@
+#include "icon.h"
+
+#include <fcft/fcft.h>
+#include <pixman.h>
+
+#include <ctype.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define PREMUL_ALPHA(chan, alpha) (chan * alpha + 127) / 255
+
+/*
+ * Converts pixels from uint8_t[4] to uint32_t and
+ * straight alpha to premultiplied alpha.
+ */
+static uint32_t *
+to_pixman(const uint8_t *src, int n_pixels, size_t *pix_size)
+{
+	uint32_t *dest = NULL;
+
+	*pix_size = n_pixels * sizeof(uint32_t);
+	dest = malloc(*pix_size);
+	if (!dest)
+		return NULL;
+
+	for (int i = 0; i < n_pixels; i++) {
+		uint8_t a = src[i * 4 + 0];
+		uint8_t r = src[i * 4 + 1];
+		uint8_t g = src[i * 4 + 2];
+		uint8_t b = src[i * 4 + 3];
+
+		/*
+		 * Skip premultiplying fully opaque and fully transparent
+		 * pixels.
+		 */
+		if (a == 0) {
+			dest[i] = 0;
+
+		} else if (a == 255) {
+			dest[i] = ((uint32_t)a << 24) | ((uint32_t)r << 16) |
+			          ((uint32_t)g << 8) | ((uint32_t)b);
+
+		} else {
+			dest[i] = ((uint32_t)a << 24) |
+			          ((uint32_t)PREMUL_ALPHA(r, a) << 16) |
+			          ((uint32_t)PREMUL_ALPHA(g, a) << 8) |
+			          ((uint32_t)PREMUL_ALPHA(b, a));
+		}
+	}
+
+	return dest;
+}
+
+Icon *
+createicon(const uint8_t *buf, int width, int height, int size)
+{
+	Icon *icon = NULL;
+
+	int n_pixels;
+	pixman_image_t *img = NULL;
+	size_t pixbuf_size;
+	uint32_t *buf_pixman = NULL;
+	uint8_t *buf_orig = NULL;
+
+	n_pixels = size / 4;
+
+	icon = calloc(1, sizeof(Icon));
+	buf_orig = malloc(size);
+	buf_pixman = to_pixman(buf, n_pixels, &pixbuf_size);
+	if (!icon || !buf_orig || !buf_pixman)
+		goto fail;
+
+	img = pixman_image_create_bits(PIXMAN_a8r8g8b8, width, height,
+	                               buf_pixman, width * 4);
+	if (!img)
+		goto fail;
+
+	memcpy(buf_orig, buf, size);
+
+	icon->buf_orig = buf_orig;
+	icon->buf_pixman = buf_pixman;
+	icon->img = img;
+	icon->size_orig = size;
+	icon->size_pixman = pixbuf_size;
+
+	return icon;
+
+fail:
+	free(buf_orig);
+	if (img)
+		pixman_image_unref(img);
+	free(buf_pixman);
+	free(icon);
+	return NULL;
+}
+
+void
+destroyicon(Icon *icon)
+{
+	if (icon->img)
+		pixman_image_unref(icon->img);
+	free(icon->buf_orig);
+	free(icon->buf_pixman);
+	free(icon);
+}
+
+FallbackIcon *
+createfallbackicon(const char *appname, int fgcolor, struct fcft_font *font)
+{
+	const struct fcft_glyph *glyph;
+	char initial;
+
+	if ((unsigned char)appname[0] > 127) {
+		/* first character is not ascii */
+		initial = '?';
+	} else {
+		initial = toupper(*appname);
+	}
+
+	glyph = fcft_rasterize_char_utf32(font, initial, FCFT_SUBPIXEL_DEFAULT);
+	if (!glyph)
+		return NULL;
+
+	return glyph;
+}
+
+int
+resize_image(pixman_image_t *image, int new_width, int new_height)
+{
+	int src_width = pixman_image_get_width(image);
+	int src_height = pixman_image_get_height(image);
+	pixman_transform_t transform;
+	pixman_fixed_t scale_x, scale_y;
+
+	if (src_width == new_width && src_height == new_height)
+		return 0;
+
+	scale_x = pixman_double_to_fixed((double)src_width / new_width);
+	scale_y = pixman_double_to_fixed((double)src_height / new_height);
+
+	pixman_transform_init_scale(&transform, scale_x, scale_y);
+	if (!pixman_image_set_filter(image, PIXMAN_FILTER_BEST, NULL, 0) ||
+	    !pixman_image_set_transform(image, &transform)) {
+		return -1;
+	}
+
+	return 0;
+}
diff --git a/systray/icon.h b/systray/icon.h
new file mode 100644
index 0000000..20f281b
--- /dev/null
+++ b/systray/icon.h
@@ -0,0 +1,26 @@
+#ifndef ICON_H
+#define ICON_H
+
+#include <fcft/fcft.h>
+#include <pixman.h>
+
+#include <stddef.h>
+#include <stdint.h>
+
+typedef const struct fcft_glyph FallbackIcon;
+
+typedef struct {
+	pixman_image_t *img;
+	uint32_t *buf_pixman;
+	uint8_t *buf_orig;
+	size_t size_orig;
+	size_t size_pixman;
+} Icon;
+
+Icon *createicon (const uint8_t *buf, int width, int height, int size);
+FallbackIcon *createfallbackicon (const char *appname, int fgcolor,
+                                  struct fcft_font *font);
+void destroyicon (Icon *icon);
+int resize_image (pixman_image_t *orig, int new_width, int new_height);
+
+#endif /* ICON_H */
diff --git a/systray/item.c b/systray/item.c
new file mode 100644
index 0000000..4359a28
--- /dev/null
+++ b/systray/item.c
@@ -0,0 +1,403 @@
+#include "item.h"
+
+#include "helpers.h"
+#include "icon.h"
+#include "watcher.h"
+
+#include <dbus/dbus.h>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+// IWYU pragma: no_include "dbus/dbus-protocol.h"
+// IWYU pragma: no_include "dbus/dbus-shared.h"
+
+#define RULEBSIZE 256
+#define MIN(A, B) ((A) < (B) ? (A) : (B))
+
+static const char *match_string =
+	"type='signal',"
+	"sender='%s',"
+	"interface='" SNI_NAME
+	"',"
+	"member='NewIcon'";
+
+static Watcher *
+item_get_watcher(const Item *item)
+{
+	if (!item)
+		return NULL;
+
+	return item->watcher;
+}
+
+static DBusConnection *
+item_get_connection(const Item *item)
+{
+	if (!item || !item->watcher)
+		return NULL;
+
+	return item->watcher->conn;
+}
+
+static const uint8_t *
+extract_image(DBusMessageIter *iter, dbus_int32_t *width, dbus_int32_t *height,
+              int *size)
+{
+	DBusMessageIter vals, bytes;
+	const uint8_t *buf;
+
+	dbus_message_iter_recurse(iter, &vals);
+	if (dbus_message_iter_get_arg_type(&vals) != DBUS_TYPE_INT32)
+		goto fail;
+	dbus_message_iter_get_basic(&vals, width);
+
+	dbus_message_iter_next(&vals);
+	if (dbus_message_iter_get_arg_type(&vals) != DBUS_TYPE_INT32)
+		goto fail;
+	dbus_message_iter_get_basic(&vals, height);
+
+	dbus_message_iter_next(&vals);
+	if (dbus_message_iter_get_arg_type(&vals) != DBUS_TYPE_ARRAY)
+		goto fail;
+	dbus_message_iter_recurse(&vals, &bytes);
+	if (dbus_message_iter_get_arg_type(&bytes) != DBUS_TYPE_BYTE)
+		goto fail;
+	dbus_message_iter_get_fixed_array(&bytes, &buf, size);
+	if (size == 0)
+		goto fail;
+
+	return buf;
+
+fail:
+	return NULL;
+}
+
+static int
+select_image(DBusMessageIter *iter, int target_width)
+{
+	DBusMessageIter vals;
+	dbus_int32_t cur_width;
+	int i = 0;
+
+	do {
+		dbus_message_iter_recurse(iter, &vals);
+		if (dbus_message_iter_get_arg_type(&vals) != DBUS_TYPE_INT32)
+			return -1;
+		dbus_message_iter_get_basic(&vals, &cur_width);
+		if (cur_width >= target_width)
+			return i;
+
+		i++;
+	} while (dbus_message_iter_next(iter));
+
+	/* return last index if desired not found */
+	return --i;
+}
+
+static void
+menupath_ready_handler(DBusPendingCall *pending, void *data)
+{
+	Item *item = data;
+
+	DBusError err = DBUS_ERROR_INIT;
+	DBusMessage *reply = NULL;
+	DBusMessageIter iter, opath;
+	char *path_dup = NULL;
+	const char *path;
+
+	reply = dbus_pending_call_steal_reply(pending);
+	if (!reply)
+		goto fail;
+
+	if (dbus_set_error_from_message(&err, reply)) {
+		fprintf(stderr, "DBus Error: %s - %s: Couldn't get menupath\n",
+		        err.name, err.message);
+		goto fail;
+	}
+
+	dbus_message_iter_init(reply, &iter);
+	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT)
+		goto fail;
+	dbus_message_iter_recurse(&iter, &opath);
+	if (dbus_message_iter_get_arg_type(&opath) != DBUS_TYPE_OBJECT_PATH)
+		goto fail;
+	dbus_message_iter_get_basic(&opath, &path);
+
+	path_dup = strdup(path);
+	if (!path_dup)
+		goto fail;
+
+	item->menu_busobj = path_dup;
+
+	dbus_message_unref(reply);
+	dbus_pending_call_unref(pending);
+	return;
+
+fail:
+	free(path_dup);
+	dbus_error_free(&err);
+	if (reply)
+		dbus_message_unref(reply);
+	if (pending)
+		dbus_pending_call_unref(pending);
+}
+
+/*
+ * Gets the Id dbus property, which is the name of the application,
+ * most of the time...
+ * The initial letter will be used as a fallback icon
+ */
+static void
+id_ready_handler(DBusPendingCall *pending, void *data)
+{
+	Item *item = data;
+
+	DBusError err = DBUS_ERROR_INIT;
+	DBusMessage *reply = NULL;
+	DBusMessageIter iter, string;
+	Watcher *watcher;
+	char *id_dup = NULL;
+	const char *id;
+
+	watcher = item_get_watcher(item);
+
+	reply = dbus_pending_call_steal_reply(pending);
+	if (!reply)
+		goto fail;
+
+	if (dbus_set_error_from_message(&err, reply)) {
+		fprintf(stderr, "DBus Error: %s - %s: Couldn't get appid\n",
+		        err.name, err.message);
+		goto fail;
+	}
+
+	dbus_message_iter_init(reply, &iter);
+	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT)
+		goto fail;
+	dbus_message_iter_recurse(&iter, &string);
+	if (dbus_message_iter_get_arg_type(&string) != DBUS_TYPE_STRING)
+		goto fail;
+	dbus_message_iter_get_basic(&string, &id);
+
+	id_dup = strdup(id);
+	if (!id_dup)
+		goto fail;
+	item->appid = id_dup;
+
+	/* Don't trigger update if this item already has a real icon */
+	if (!item->icon)
+		watcher_update_trays(watcher);
+
+	dbus_message_unref(reply);
+	dbus_pending_call_unref(pending);
+	return;
+
+fail:
+	dbus_error_free(&err);
+	if (id_dup)
+		free(id_dup);
+	if (reply)
+		dbus_message_unref(reply);
+	if (pending)
+		dbus_pending_call_unref(pending);
+}
+
+static void
+pixmap_ready_handler(DBusPendingCall *pending, void *data)
+{
+	Item *item = data;
+
+	DBusMessage *reply = NULL;
+	DBusMessageIter iter, array, select, strct;
+	Icon *icon = NULL;
+	Watcher *watcher;
+	dbus_int32_t width, height;
+	int selected_index, size;
+	const uint8_t *buf;
+
+	watcher = item_get_watcher(item);
+
+	reply = dbus_pending_call_steal_reply(pending);
+	if (!reply || dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR)
+		goto fail;
+	dbus_message_iter_init(reply, &iter);
+	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT)
+		goto fail;
+	dbus_message_iter_recurse(&iter, &array);
+	if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_ARRAY)
+		goto fail;
+	dbus_message_iter_recurse(&array, &select);
+	if (dbus_message_iter_get_arg_type(&select) != DBUS_TYPE_STRUCT)
+		goto fail;
+	selected_index = select_image(&select, 22); // Get the 22*22 image
+	if (selected_index < 0)
+		goto fail;
+
+	dbus_message_iter_recurse(&array, &strct);
+	if (dbus_message_iter_get_arg_type(&strct) != DBUS_TYPE_STRUCT)
+		goto fail;
+	for (int i = 0; i < selected_index; i++)
+		dbus_message_iter_next(&strct);
+	buf = extract_image(&strct, &width, &height, &size);
+	if (!buf)
+		goto fail;
+
+	if (!item->icon) {
+		/* First icon */
+		icon = createicon(buf, width, height, size);
+		if (!icon)
+			goto fail;
+		item->icon = icon;
+		watcher_update_trays(watcher);
+
+	} else if (memcmp(item->icon->buf_orig, buf,
+	                  MIN(item->icon->size_orig, (size_t)size)) != 0) {
+		/* New icon */
+		destroyicon(item->icon);
+		item->icon = NULL;
+		icon = createicon(buf, width, height, size);
+		if (!icon)
+			goto fail;
+		item->icon = icon;
+		watcher_update_trays(watcher);
+
+	} else {
+		/* Icon didn't change */
+	}
+
+	dbus_message_unref(reply);
+	dbus_pending_call_unref(pending);
+	return;
+
+fail:
+	if (icon)
+		destroyicon(icon);
+	if (reply)
+		dbus_message_unref(reply);
+	if (pending)
+		dbus_pending_call_unref(pending);
+}
+
+static DBusHandlerResult
+handle_newicon(Item *item, DBusConnection *conn, DBusMessage *msg)
+{
+	const char *sender = dbus_message_get_sender(msg);
+
+	if (sender && strcmp(sender, item->busname) == 0) {
+		request_property(conn, item->busname, item->busobj,
+		                 "IconPixmap", SNI_IFACE, pixmap_ready_handler,
+		                 item);
+
+		return DBUS_HANDLER_RESULT_HANDLED;
+
+	} else {
+		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+	}
+}
+
+static DBusHandlerResult
+filter_bus(DBusConnection *conn, DBusMessage *msg, void *data)
+{
+	Item *item = data;
+
+	if (dbus_message_is_signal(msg, SNI_IFACE, "NewIcon"))
+		return handle_newicon(item, conn, msg);
+	else
+		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+Item *
+createitem(const char *busname, const char *busobj, Watcher *watcher)
+{
+	DBusConnection *conn;
+	Item *item;
+	char *busname_dup = NULL;
+	char *busobj_dup = NULL;
+	char match_rule[RULEBSIZE];
+
+	item = calloc(1, sizeof(Item));
+	busname_dup = strdup(busname);
+	busobj_dup = strdup(busobj);
+	if (!item || !busname_dup || !busobj_dup)
+		goto fail;
+
+	conn = watcher->conn;
+	item->busname = busname_dup;
+	item->busobj = busobj_dup;
+	item->watcher = watcher;
+
+	request_property(conn, busname, busobj, "IconPixmap", SNI_IFACE,
+	                 pixmap_ready_handler, item);
+
+	request_property(conn, busname, busobj, "Id", SNI_IFACE,
+	                 id_ready_handler, item);
+
+	request_property(conn, busname, busobj, "Menu", SNI_IFACE,
+	                 menupath_ready_handler, item);
+
+	if (snprintf(match_rule, sizeof(match_rule), match_string, busname) >=
+	    RULEBSIZE) {
+		goto fail;
+	}
+
+	if (!dbus_connection_add_filter(conn, filter_bus, item, NULL))
+		goto fail;
+	dbus_bus_add_match(conn, match_rule, NULL);
+
+	return item;
+
+fail:
+	free(busname_dup);
+	free(busobj_dup);
+	return NULL;
+}
+
+void
+destroyitem(Item *item)
+{
+	DBusConnection *conn;
+	char match_rule[RULEBSIZE];
+
+	conn = item_get_connection(item);
+
+	if (snprintf(match_rule, sizeof(match_rule), match_string,
+	             item->busname) < RULEBSIZE) {
+		dbus_bus_remove_match(conn, match_rule, NULL);
+		dbus_connection_remove_filter(conn, filter_bus, item);
+	}
+	if (item->icon)
+		destroyicon(item->icon);
+	free(item->menu_busobj);
+	free(item->busname);
+	free(item->busobj);
+	free(item->appid);
+	free(item);
+}
+
+void
+item_activate(Item *item)
+{
+	DBusConnection *conn;
+	DBusMessage *msg = NULL;
+	dbus_int32_t x = 0, y = 0;
+
+	conn = item_get_connection(item);
+
+	if (!(msg = dbus_message_new_method_call(item->busname, item->busobj,
+	                                         SNI_IFACE, "Activate")) ||
+	    !dbus_message_append_args(msg, DBUS_TYPE_INT32, &x, DBUS_TYPE_INT32,
+	                              &y, DBUS_TYPE_INVALID) ||
+	    !dbus_connection_send_with_reply(conn, msg, NULL, -1)) {
+		goto fail;
+	}
+
+	dbus_message_unref(msg);
+	return;
+
+fail:
+	if (msg)
+		dbus_message_unref(msg);
+}
diff --git a/systray/item.h b/systray/item.h
new file mode 100644
index 0000000..dc22e25
--- /dev/null
+++ b/systray/item.h
@@ -0,0 +1,46 @@
+#ifndef ITEM_H
+#define ITEM_H
+
+#include "icon.h"
+#include "watcher.h"
+
+#include <wayland-util.h>
+
+/*
+ * The FDO spec says "org.freedesktop.StatusNotifierItem"[1],
+ * but both the client libraries[2,3] actually use "org.kde.StatusNotifierItem"
+ *
+ * [1] https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/
+ * [2] https://github.com/AyatanaIndicators/libayatana-appindicator-glib
+ * [3] https://invent.kde.org/frameworks/kstatusnotifieritem
+ *
+ */
+#define SNI_NAME "org.kde.StatusNotifierItem"
+#define SNI_OPATH "/StatusNotifierItem"
+#define SNI_IFACE "org.kde.StatusNotifierItem"
+
+typedef struct Item {
+	struct wl_list icons;
+	char *busname;
+	char *busobj;
+	char *menu_busobj;
+	char *appid;
+	Icon *icon;
+	FallbackIcon *fallback_icon;
+
+	Watcher *watcher;
+
+	int fgcolor;
+
+	int ready;
+
+	struct wl_list link;
+} Item;
+
+Item *createitem (const char *busname, const char *busobj, Watcher *watcher);
+void destroyitem (Item *item);
+
+void item_activate (Item *item);
+void item_show_menu (Item *item);
+
+#endif /* ITEM_H */
diff --git a/systray/menu.c b/systray/menu.c
new file mode 100644
index 0000000..ff3bfb5
--- /dev/null
+++ b/systray/menu.c
@@ -0,0 +1,757 @@
+#include "menu.h"
+
+#include <dbus/dbus.h>
+#include <wayland-server-core.h>
+#include <wayland-util.h>
+
+#include <errno.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+// IWYU pragma: no_include "dbus/dbus-protocol.h"
+// IWYU pragma: no_include "dbus/dbus-shared.h"
+
+#define DBUSMENU_IFACE "com.canonical.dbusmenu"
+#define BUFSIZE 512
+#define LABEL_MAX 64
+
+typedef struct {
+	struct wl_array layout;
+	DBusConnection *conn;
+	struct wl_event_loop *loop;
+	char *busname;
+	char *busobj;
+	const char **menucmd;
+} Menu;
+
+typedef struct {
+	char label[LABEL_MAX];
+	dbus_int32_t id;
+	struct wl_array submenu;
+	int has_submenu;
+} MenuItem;
+
+typedef struct {
+	struct wl_event_loop *loop;
+	struct wl_event_source *fd_source;
+	struct wl_array *layout_node;
+	Menu *menu;
+	pid_t menu_pid;
+	int fd;
+} MenuShowContext;
+
+static int extract_menu (DBusMessageIter *av, struct wl_array *menu);
+static int real_show_menu (Menu *menu, struct wl_array *m);
+static void submenus_destroy_recursive (struct wl_array *m);
+
+static void
+menuitem_init(MenuItem *mi)
+{
+	wl_array_init(&mi->submenu);
+	mi->id = -1;
+	*mi->label = '\0';
+	mi->has_submenu = 0;
+}
+
+static void
+submenus_destroy_recursive(struct wl_array *layout_node)
+{
+	MenuItem *mi;
+
+	wl_array_for_each(mi, layout_node) {
+		if (mi->has_submenu) {
+			submenus_destroy_recursive(&mi->submenu);
+			wl_array_release(&mi->submenu);
+		}
+	}
+}
+
+static void
+menu_destroy(Menu *menu)
+{
+	submenus_destroy_recursive(&menu->layout);
+	wl_array_release(&menu->layout);
+	free(menu->busname);
+	free(menu->busobj);
+	free(menu);
+}
+
+static void
+menu_show_ctx_finalize(MenuShowContext *ctx, int error)
+{
+	if (ctx->fd_source)
+		wl_event_source_remove(ctx->fd_source);
+
+	if (ctx->fd >= 0)
+		close(ctx->fd);
+
+	if (ctx->menu_pid >= 0) {
+		if (waitpid(ctx->menu_pid, NULL, WNOHANG) == 0)
+			kill(ctx->menu_pid, SIGTERM);
+	}
+
+	if (error)
+		menu_destroy(ctx->menu);
+
+	free(ctx);
+}
+
+static void
+remove_newline(char *buf)
+{
+	size_t len;
+
+	len = strlen(buf);
+	if (len > 0 && buf[len - 1] == '\n')
+		buf[len - 1] = '\0';
+}
+
+static void
+send_clicked(const char *busname, const char *busobj, int itemid,
+             DBusConnection *conn)
+{
+	DBusMessage *msg = NULL;
+	DBusMessageIter iter = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	DBusMessageIter sub = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	const char *data = "";
+	const char *eventid = "clicked";
+	time_t timestamp;
+
+	timestamp = time(NULL);
+
+	msg = dbus_message_new_method_call(busname, busobj, DBUSMENU_IFACE,
+	                                   "Event");
+	if (!msg)
+		goto fail;
+
+	dbus_message_iter_init_append(msg, &iter);
+	if (!dbus_message_iter_append_basic(&iter, DBUS_TYPE_INT32, &itemid) ||
+	    !dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING,
+	                                    &eventid) ||
+	    !dbus_message_iter_open_container(&iter, DBUS_TYPE_VARIANT,
+	                                      DBUS_TYPE_STRING_AS_STRING,
+	                                      &sub) ||
+	    !dbus_message_iter_append_basic(&sub, DBUS_TYPE_STRING, &data) ||
+	    !dbus_message_iter_close_container(&iter, &sub) ||
+	    !dbus_message_iter_append_basic(&iter, DBUS_TYPE_UINT32,
+	                                    &timestamp)) {
+		goto fail;
+	}
+
+	if (!dbus_connection_send_with_reply(conn, msg, NULL, -1))
+		goto fail;
+
+	dbus_message_unref(msg);
+	return;
+
+fail:
+	dbus_message_iter_abandon_container_if_open(&iter, &sub);
+	if (msg)
+		dbus_message_unref(msg);
+}
+
+static void
+menuitem_selected(const char *label, struct wl_array *m, Menu *menu)
+{
+	MenuItem *mi;
+
+	wl_array_for_each(mi, m) {
+		if (strcmp(mi->label, label) == 0) {
+			if (mi->has_submenu) {
+				real_show_menu(menu, &mi->submenu);
+
+			} else {
+				send_clicked(menu->busname, menu->busobj,
+				             mi->id, menu->conn);
+				menu_destroy(menu);
+			}
+
+			return;
+		}
+	}
+}
+
+static int
+read_pipe(int fd, uint32_t mask, void *data)
+{
+	MenuShowContext *ctx = data;
+
+	char buf[BUFSIZE];
+	ssize_t bytes_read;
+
+	bytes_read = read(fd, buf, BUFSIZE);
+	/* 0 == Got EOF, menu program closed without writing to stdout */
+	if (bytes_read <= 0)
+		goto fail;
+
+	buf[bytes_read] = '\0';
+	remove_newline(buf);
+
+	menuitem_selected(buf, ctx->layout_node, ctx->menu);
+	menu_show_ctx_finalize(ctx, 0);
+	return 0;
+
+fail:
+	menu_show_ctx_finalize(ctx, 1);
+	return 0;
+}
+
+static MenuShowContext *
+prepare_show_ctx(struct wl_event_loop *loop, int monitor_fd, int dmenu_pid,
+                 struct wl_array *layout_node, Menu *menu)
+{
+	MenuShowContext *ctx = NULL;
+	struct wl_event_source *fd_src = NULL;
+
+	ctx = calloc(1, sizeof(MenuShowContext));
+	if (!ctx)
+		goto fail;
+
+	fd_src = wl_event_loop_add_fd(menu->loop, monitor_fd, WL_EVENT_READABLE,
+	                              read_pipe, ctx);
+	if (!fd_src)
+		goto fail;
+
+	ctx->fd_source = fd_src;
+	ctx->fd = monitor_fd;
+	ctx->menu_pid = dmenu_pid;
+	ctx->layout_node = layout_node;
+	ctx->menu = menu;
+
+	return ctx;
+
+fail:
+	if (fd_src)
+		wl_event_source_remove(fd_src);
+	free(ctx);
+	return NULL;
+}
+
+static int
+write_dmenu_buf(char *buf, struct wl_array *layout_node)
+{
+	MenuItem *mi;
+	int r;
+	size_t curlen = 0;
+
+	*buf = '\0';
+
+	wl_array_for_each(mi, layout_node) {
+		curlen += strlen(mi->label) +
+		          2; /* +2 is newline + nul terminator */
+		if (curlen + 1 > BUFSIZE) {
+			r = -1;
+			goto fail;
+		}
+
+		strcat(buf, mi->label);
+		strcat(buf, "\n");
+	}
+	remove_newline(buf);
+
+	return 0;
+
+fail:
+	fprintf(stderr, "Failed to construct dmenu input\n");
+	return r;
+}
+
+static int
+real_show_menu(Menu *menu, struct wl_array *layout_node)
+{
+	MenuShowContext *ctx = NULL;
+	char buf[BUFSIZE];
+	int to_pipe[2], from_pipe[2];
+	pid_t pid;
+
+	if (pipe(to_pipe) < 0 || pipe(from_pipe) < 0)
+		goto fail;
+
+	pid = fork();
+	if (pid < 0) {
+		goto fail;
+	} else if (pid == 0) {
+		dup2(to_pipe[0], STDIN_FILENO);
+		dup2(from_pipe[1], STDOUT_FILENO);
+
+		close(to_pipe[0]);
+		close(to_pipe[1]);
+		close(from_pipe[1]);
+		close(from_pipe[0]);
+
+		if (execvp(menu->menucmd[0], (char *const *)menu->menucmd)) {
+			perror("Error spawning menu program");
+			exit(EXIT_FAILURE);
+		}
+	}
+
+	ctx = prepare_show_ctx(menu->loop, from_pipe[0], pid, layout_node,
+	                       menu);
+	if (!ctx)
+		goto fail;
+
+	if (write_dmenu_buf(buf, layout_node) < 0 ||
+	    write(to_pipe[1], buf, strlen(buf)) < 0) {
+		goto fail;
+	}
+
+	close(to_pipe[0]);
+	close(to_pipe[1]);
+	close(from_pipe[1]);
+	return 0;
+
+fail:
+	close(to_pipe[0]);
+	close(to_pipe[1]);
+	close(from_pipe[1]);
+	menu_show_ctx_finalize(ctx, 1);
+	return -1;
+}
+
+static void
+createmenuitem(MenuItem *mi, dbus_int32_t id, const char *label,
+               int toggle_state, int has_submenu)
+{
+	char *tok;
+	char temp[LABEL_MAX];
+
+	if (toggle_state == 0)
+		strcpy(mi->label, "☐ ");
+	else if (toggle_state == 1)
+		strcpy(mi->label, "✓ ");
+	else
+		strcpy(mi->label, "  ");
+
+	/* Remove "mnemonics" (underscores which mark keyboard shortcuts) */
+	strcpy(temp, label);
+	tok = strtok(temp, "_");
+	do {
+		strcat(mi->label, tok);
+	} while ((tok = strtok(NULL, "_")));
+
+	if (has_submenu) {
+		mi->has_submenu = 1;
+		strcat(mi->label, " →");
+	}
+
+	mi->id = id;
+}
+
+/**
+ * Populates the passed in menuitem based on the dictionary contents.
+ *
+ * @param[in] dict
+ * @param[in] itemid
+ * @param[in] mi
+ * @param[out] has_submenu
+ * @param[out] status <0 on error, 0 on success, >0 if menuitem was skipped
+ */
+static int
+read_dict(DBusMessageIter *dict, dbus_int32_t itemid, MenuItem *mi,
+          int *has_submenu)
+{
+	DBusMessageIter member, val;
+	const char *children_display = NULL, *label = NULL, *toggle_type = NULL;
+	const char *key;
+	dbus_bool_t visible = TRUE, enabled = TRUE;
+	dbus_int32_t toggle_state = 1;
+	int r;
+
+	do {
+		dbus_message_iter_recurse(dict, &member);
+		if (dbus_message_iter_get_arg_type(&member) !=
+		    DBUS_TYPE_STRING) {
+			r = -1;
+			goto fail;
+		}
+		dbus_message_iter_get_basic(&member, &key);
+
+		dbus_message_iter_next(&member);
+		if (dbus_message_iter_get_arg_type(&member) !=
+		    DBUS_TYPE_VARIANT) {
+			r = -1;
+			goto fail;
+		}
+		dbus_message_iter_recurse(&member, &val);
+
+		if (strcmp(key, "visible") == 0) {
+			if (dbus_message_iter_get_arg_type(&val) !=
+			    DBUS_TYPE_BOOLEAN) {
+				r = -1;
+				goto fail;
+			}
+			dbus_message_iter_get_basic(&val, &visible);
+
+		} else if (strcmp(key, "enabled") == 0) {
+			if (dbus_message_iter_get_arg_type(&val) !=
+			    DBUS_TYPE_BOOLEAN) {
+				r = -1;
+				goto fail;
+			}
+			dbus_message_iter_get_basic(&val, &enabled);
+
+		} else if (strcmp(key, "toggle-type") == 0) {
+			if (dbus_message_iter_get_arg_type(&val) !=
+			    DBUS_TYPE_STRING) {
+				r = -1;
+				goto fail;
+			}
+			dbus_message_iter_get_basic(&val, &toggle_type);
+
+		} else if (strcmp(key, "toggle-state") == 0) {
+			if (dbus_message_iter_get_arg_type(&val) !=
+			    DBUS_TYPE_INT32) {
+				r = -1;
+				goto fail;
+			}
+			dbus_message_iter_get_basic(&val, &toggle_state);
+
+		} else if (strcmp(key, "children-display") == 0) {
+			if (dbus_message_iter_get_arg_type(&val) !=
+			    DBUS_TYPE_STRING) {
+				r = -1;
+				goto fail;
+			}
+			dbus_message_iter_get_basic(&val, &children_display);
+
+			if (strcmp(children_display, "submenu") == 0)
+				*has_submenu = 1;
+
+		} else if (strcmp(key, "label") == 0) {
+			if (dbus_message_iter_get_arg_type(&val) !=
+			    DBUS_TYPE_STRING) {
+				r = -1;
+				goto fail;
+			}
+			dbus_message_iter_get_basic(&val, &label);
+		}
+	} while (dbus_message_iter_next(dict));
+
+	/* Skip hidden etc items */
+	if (!label || !visible || !enabled)
+		return 1;
+
+	/*
+	 * 4 characters for checkmark and submenu indicator,
+	 * 1 for nul terminator
+	 */
+	if (strlen(label) + 5 > LABEL_MAX) {
+		fprintf(stderr, "Too long menu entry label: %s! Skipping...\n",
+		        label);
+		return 1;
+	}
+
+	if (toggle_type && strcmp(toggle_type, "checkmark") == 0)
+		createmenuitem(mi, itemid, label, toggle_state, *has_submenu);
+	else
+		createmenuitem(mi, itemid, label, -1, *has_submenu);
+
+	return 0;
+
+fail:
+	fprintf(stderr, "Error parsing menu data\n");
+	return r;
+}
+
+/**
+ * Extracts a menuitem from a DBusMessage
+ *
+ * @param[in] strct
+ * @param[in] mi
+ * @param[out] status <0 on error, 0 on success, >0 if menuitem was skipped
+ */
+static int
+extract_menuitem(DBusMessageIter *strct, MenuItem *mi)
+{
+	DBusMessageIter val, dict;
+	dbus_int32_t itemid;
+	int has_submenu = 0;
+	int r;
+
+	dbus_message_iter_recurse(strct, &val);
+	if (dbus_message_iter_get_arg_type(&val) != DBUS_TYPE_INT32) {
+		r = -1;
+		goto fail;
+	}
+	dbus_message_iter_get_basic(&val, &itemid);
+
+	if (!dbus_message_iter_next(&val) ||
+	    dbus_message_iter_get_arg_type(&val) != DBUS_TYPE_ARRAY) {
+		r = -1;
+		goto fail;
+	}
+	dbus_message_iter_recurse(&val, &dict);
+	if (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_DICT_ENTRY) {
+		r = -1;
+		goto fail;
+	}
+
+	r = read_dict(&dict, itemid, mi, &has_submenu);
+	if (r < 0) {
+		goto fail;
+
+	} else if (r == 0 && has_submenu) {
+		dbus_message_iter_next(&val);
+		if (dbus_message_iter_get_arg_type(&val) != DBUS_TYPE_ARRAY)
+			goto fail;
+		r = extract_menu(&val, &mi->submenu);
+		if (r < 0)
+			goto fail;
+	}
+
+	return r;
+
+fail:
+	return r;
+}
+
+static int
+extract_menu(DBusMessageIter *av, struct wl_array *layout_node)
+{
+	DBusMessageIter variant, menuitem;
+	MenuItem *mi;
+	int r;
+
+	dbus_message_iter_recurse(av, &variant);
+	if (dbus_message_iter_get_arg_type(&variant) != DBUS_TYPE_VARIANT) {
+		r = -1;
+		goto fail;
+	}
+
+	mi = wl_array_add(layout_node, sizeof(MenuItem));
+	if (!mi) {
+		r = -ENOMEM;
+		goto fail;
+	}
+	menuitem_init(mi);
+
+	do {
+		dbus_message_iter_recurse(&variant, &menuitem);
+		if (dbus_message_iter_get_arg_type(&menuitem) !=
+		    DBUS_TYPE_STRUCT) {
+			r = -1;
+			goto fail;
+		}
+
+		r = extract_menuitem(&menuitem, mi);
+		if (r < 0)
+			goto fail;
+		else if (r == 0) {
+			mi = wl_array_add(layout_node, sizeof(MenuItem));
+			if (!mi) {
+				r = -ENOMEM;
+				goto fail;
+			}
+			menuitem_init(mi);
+		}
+		/* r > 0: no action was performed on mi */
+	} while (dbus_message_iter_next(&variant));
+
+	return 0;
+
+fail:
+	return r;
+}
+
+static void
+layout_ready(DBusPendingCall *pending, void *data)
+{
+	Menu *menu = data;
+
+	DBusMessage *reply = NULL;
+	DBusMessageIter iter, strct;
+	dbus_uint32_t revision;
+	int r;
+
+	reply = dbus_pending_call_steal_reply(pending);
+	if (!reply || dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
+		r = -1;
+		goto fail;
+	}
+
+	dbus_message_iter_init(reply, &iter);
+	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) {
+		r = -1;
+		goto fail;
+	}
+	dbus_message_iter_get_basic(&iter, &revision);
+
+	if (!dbus_message_iter_next(&iter) ||
+	    dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRUCT) {
+		r = -1;
+		goto fail;
+	}
+	dbus_message_iter_recurse(&iter, &strct);
+
+	/*
+	 * id 0 is the root, which contains nothing of interest.
+	 * Traverse past it.
+	 */
+	if (dbus_message_iter_get_arg_type(&strct) != DBUS_TYPE_INT32 ||
+	    !dbus_message_iter_next(&strct) ||
+	    dbus_message_iter_get_arg_type(&strct) != DBUS_TYPE_ARRAY ||
+	    !dbus_message_iter_next(&strct) ||
+	    dbus_message_iter_get_arg_type(&strct) != DBUS_TYPE_ARRAY) {
+		r = -1;
+		goto fail;
+	}
+
+	/* Root traversed over, extract the menu */
+	wl_array_init(&menu->layout);
+	r = extract_menu(&strct, &menu->layout);
+	if (r < 0)
+		goto fail;
+
+	r = real_show_menu(menu, &menu->layout);
+	if (r < 0)
+		goto fail;
+
+	dbus_message_unref(reply);
+	dbus_pending_call_unref(pending);
+	return;
+
+fail:
+	menu_destroy(menu);
+	if (reply)
+		dbus_message_unref(reply);
+	if (pending)
+		dbus_pending_call_unref(pending);
+}
+
+static int
+request_layout(Menu *menu)
+{
+	DBusMessage *msg = NULL;
+	DBusMessageIter iter = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	DBusMessageIter strings = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	DBusPendingCall *pending = NULL;
+	dbus_int32_t parentid, depth;
+	int r;
+
+	parentid = 0;
+	depth = -1;
+
+	/* menu busobj request answer didn't arrive yet. */
+	if (!menu->busobj) {
+		r = -1;
+		goto fail;
+	}
+
+	msg = dbus_message_new_method_call(menu->busname, menu->busobj,
+	                                   DBUSMENU_IFACE, "GetLayout");
+	if (!msg) {
+		r = -ENOMEM;
+		goto fail;
+	}
+
+	dbus_message_iter_init_append(msg, &iter);
+	if (!dbus_message_iter_append_basic(&iter, DBUS_TYPE_INT32,
+	                                    &parentid) ||
+	    !dbus_message_iter_append_basic(&iter, DBUS_TYPE_INT32, &depth) ||
+	    !dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY,
+	                                      DBUS_TYPE_STRING_AS_STRING,
+	                                      &strings) ||
+	    !dbus_message_iter_close_container(&iter, &strings)) {
+		r = -ENOMEM;
+		goto fail;
+	}
+
+	if (!dbus_connection_send_with_reply(menu->conn, msg, &pending, -1) ||
+	    !dbus_pending_call_set_notify(pending, layout_ready, menu, NULL)) {
+		r = -ENOMEM;
+		goto fail;
+	}
+
+	dbus_message_unref(msg);
+	return 0;
+
+fail:
+	if (pending) {
+		dbus_pending_call_cancel(pending);
+		dbus_pending_call_unref(pending);
+	}
+	dbus_message_iter_abandon_container_if_open(&iter, &strings);
+	if (msg)
+		dbus_message_unref(msg);
+	menu_destroy(menu);
+	return r;
+}
+
+static void
+about_to_show_handle(DBusPendingCall *pending, void *data)
+{
+	Menu *menu = data;
+
+	DBusMessage *reply = NULL;
+
+	reply = dbus_pending_call_steal_reply(pending);
+	if (!reply)
+		goto fail;
+
+	if (request_layout(menu) < 0)
+		goto fail;
+
+	dbus_message_unref(reply);
+	dbus_pending_call_unref(pending);
+	return;
+
+fail:
+	if (reply)
+		dbus_message_unref(reply);
+	if (pending)
+		dbus_pending_call_unref(pending);
+	menu_destroy(menu);
+}
+
+void
+menu_show(DBusConnection *conn, struct wl_event_loop *loop, const char *busname,
+          const char *busobj, const char **menucmd)
+{
+	DBusMessage *msg = NULL;
+	DBusPendingCall *pending = NULL;
+	Menu *menu = NULL;
+	char *busname_dup = NULL, *busobj_dup = NULL;
+	dbus_int32_t parentid = 0;
+
+	menu = calloc(1, sizeof(Menu));
+	busname_dup = strdup(busname);
+	busobj_dup = strdup(busobj);
+	if (!menu || !busname_dup || !busobj_dup)
+		goto fail;
+
+	menu->conn = conn;
+	menu->loop = loop;
+	menu->busname = busname_dup;
+	menu->busobj = busobj_dup;
+	menu->menucmd = menucmd;
+
+	msg = dbus_message_new_method_call(menu->busname, menu->busobj,
+	                                   DBUSMENU_IFACE, "AboutToShow");
+	if (!msg)
+		goto fail;
+
+	if (!dbus_message_append_args(msg, DBUS_TYPE_INT32, &parentid,
+	                              DBUS_TYPE_INVALID) ||
+	    !dbus_connection_send_with_reply(menu->conn, msg, &pending, -1) ||
+	    !dbus_pending_call_set_notify(pending, about_to_show_handle, menu,
+	                                  NULL)) {
+		goto fail;
+	}
+
+	dbus_message_unref(msg);
+	return;
+
+fail:
+	if (pending)
+		dbus_pending_call_unref(pending);
+	if (msg)
+		dbus_message_unref(msg);
+	free(menu);
+}
diff --git a/systray/menu.h b/systray/menu.h
new file mode 100644
index 0000000..7f48ada
--- /dev/null
+++ b/systray/menu.h
@@ -0,0 +1,11 @@
+#ifndef MENU_H
+#define MENU_H
+
+#include <dbus/dbus.h>
+#include <wayland-server-core.h>
+
+/* The menu is built on demand and not kept around */
+void menu_show (DBusConnection *conn, struct wl_event_loop *loop,
+                const char *busname, const char *busobj, const char **menucmd);
+
+#endif /* MENU_H */
diff --git a/systray/tray.c b/systray/tray.c
new file mode 100644
index 0000000..7f9b1b0
--- /dev/null
+++ b/systray/tray.c
@@ -0,0 +1,237 @@
+#include "tray.h"
+
+#include "icon.h"
+#include "item.h"
+#include "menu.h"
+#include "watcher.h"
+
+#include <fcft/fcft.h>
+#include <pixman.h>
+#include <wayland-util.h>
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#define PIXMAN_COLOR(hex)                        \
+	{ .red = ((hex >> 24) & 0xff) * 0x101,   \
+	  .green = ((hex >> 16) & 0xff) * 0x101, \
+	  .blue = ((hex >> 8) & 0xff) * 0x101,   \
+	  .alpha = (hex & 0xff) * 0x101 }
+
+static Watcher *
+tray_get_watcher(const Tray *tray)
+{
+	if (!tray)
+		return NULL;
+
+	return tray->watcher;
+}
+
+static pixman_image_t *
+createcanvas(int width, int height, int bgcolor)
+{
+	pixman_image_t *src, *dest;
+	pixman_color_t bgcolor_pix = PIXMAN_COLOR(bgcolor);
+
+	dest = pixman_image_create_bits(PIXMAN_a8r8g8b8, width, height, NULL,
+	                                0);
+	src = pixman_image_create_solid_fill(&bgcolor_pix);
+
+	pixman_image_composite32(PIXMAN_OP_SRC, src, NULL, dest, 0, 0, 0, 0, 0,
+	                         0, width, height);
+
+	pixman_image_unref(src);
+	return dest;
+}
+
+void
+tray_update(Tray *tray)
+{
+	Item *item;
+	Watcher *watcher;
+	int icon_size, i = 0, canvas_width, canvas_height, n_items, spacing;
+	pixman_image_t *canvas = NULL, *img;
+
+	watcher = tray_get_watcher(tray);
+	n_items = watcher_get_n_items(watcher);
+
+	if (!n_items) {
+		if (tray->image) {
+			pixman_image_unref(tray->image);
+			tray->image = NULL;
+		}
+		tray->cb(tray->monitor);
+		return;
+	}
+
+	icon_size = tray->height;
+	spacing = tray->spacing;
+	canvas_width = n_items * (icon_size + spacing) + spacing;
+	canvas_height = tray->height;
+
+	canvas = createcanvas(canvas_width, canvas_height, tray->scheme[1]);
+	if (!canvas)
+		goto fail;
+
+	wl_list_for_each(item, &watcher->items, link) {
+		int slot_x_start = spacing + i * (icon_size + spacing);
+		int slot_x_end = slot_x_start + icon_size + spacing;
+		int slot_x_width = slot_x_end - slot_x_start;
+
+		int slot_y_start = 0;
+		int slot_y_end = canvas_height;
+		int slot_y_width = slot_y_end - slot_y_start;
+
+		if (item->icon) {
+			/* Real icon */
+			img = item->icon->img;
+			if (resize_image(img, icon_size, icon_size) < 0)
+				goto fail;
+			pixman_image_composite32(PIXMAN_OP_OVER, img, NULL,
+			                         canvas, 0, 0, 0, 0,
+			                         slot_x_start, 0, canvas_width,
+			                         canvas_height);
+
+		} else if (item->appid) {
+			/* Font glyph alpha mask */
+			const struct fcft_glyph *g;
+			int pen_y, pen_x;
+			pixman_color_t fg_color = PIXMAN_COLOR(tray->scheme[0]);
+			pixman_image_t *fg;
+
+			if (item->fallback_icon) {
+				g = item->fallback_icon;
+			} else {
+				g = createfallbackicon(item->appid,
+				                       item->fgcolor,
+				                       tray->font);
+				if (!g)
+					goto fail;
+				item->fallback_icon = g;
+			}
+
+			pen_x = slot_x_start + (slot_x_width - g->width) / 2;
+			pen_y = slot_y_start + (slot_y_width - g->height) / 2;
+
+			fg = pixman_image_create_solid_fill(&fg_color);
+			pixman_image_composite32(PIXMAN_OP_OVER, fg, g->pix,
+			                         canvas, 0, 0, 0, 0, pen_x,
+			                         pen_y, canvas_width,
+			                         canvas_height);
+			pixman_image_unref(fg);
+		}
+		i++;
+	}
+
+	if (tray->image)
+		pixman_image_unref(tray->image);
+	tray->image = canvas;
+	tray->cb(tray->monitor);
+
+	return;
+
+fail:
+	if (canvas)
+		pixman_image_unref(canvas);
+	return;
+}
+
+void
+destroytray(Tray *tray)
+{
+	if (tray->image)
+		pixman_image_unref(tray->image);
+	if (tray->font)
+		fcft_destroy(tray->font);
+	free(tray);
+}
+
+Tray *
+createtray(void *monitor, int height, int spacing, uint32_t *colorscheme,
+           const char **fonts, const char *fontattrs, TrayNotifyCb cb,
+           Watcher *watcher)
+{
+	Tray *tray = NULL;
+	char fontattrs_my[128];
+	struct fcft_font *font = NULL;
+
+	sprintf(fontattrs_my, "%s:%s", fontattrs, "weight:bold");
+
+	tray = calloc(1, sizeof(Tray));
+	font = fcft_from_name(1, fonts, fontattrs_my);
+	if (!tray || !font)
+		goto fail;
+
+	tray->monitor = monitor;
+	tray->height = height;
+	tray->spacing = spacing;
+	tray->scheme = colorscheme;
+	tray->cb = cb;
+	tray->watcher = watcher;
+	tray->font = font;
+
+	return tray;
+
+fail:
+	if (font)
+		fcft_destroy(font);
+	free(tray);
+	return NULL;
+}
+
+int
+tray_get_width(const Tray *tray)
+{
+	if (tray && tray->image)
+		return pixman_image_get_width(tray->image);
+	else
+		return 0;
+}
+
+int
+tray_get_icon_width(const Tray *tray)
+{
+	if (!tray)
+		return 0;
+
+	return tray->height;
+}
+
+void
+tray_rightclicked(Tray *tray, unsigned int index, const char **menucmd)
+{
+	Item *item;
+	Watcher *watcher;
+	unsigned int count = 0;
+
+	watcher = tray_get_watcher(tray);
+
+	wl_list_for_each(item, &watcher->items, link) {
+		if (count == index) {
+			menu_show(watcher->conn, watcher->loop, item->busname,
+			          item->menu_busobj, menucmd);
+			return;
+		}
+		count++;
+	}
+}
+
+void
+tray_leftclicked(Tray *tray, unsigned int index)
+{
+	Item *item;
+	Watcher *watcher;
+	unsigned int count = 0;
+
+	watcher = tray_get_watcher(tray);
+
+	wl_list_for_each(item, &watcher->items, link) {
+		if (count == index) {
+			item_activate(item);
+			return;
+		}
+		count++;
+	}
+}
diff --git a/systray/tray.h b/systray/tray.h
new file mode 100644
index 0000000..af4e5e3
--- /dev/null
+++ b/systray/tray.h
@@ -0,0 +1,37 @@
+#ifndef TRAY_H
+#define TRAY_H
+
+#include "watcher.h"
+
+#include <pixman.h>
+#include <wayland-util.h>
+
+#include <stdint.h>
+
+typedef void (*TrayNotifyCb)(void *data);
+
+typedef struct {
+	pixman_image_t *image;
+	struct fcft_font *font;
+	uint32_t *scheme;
+	TrayNotifyCb cb;
+	Watcher *watcher;
+	void *monitor;
+	int height;
+	int spacing;
+
+	struct wl_list link;
+} Tray;
+
+Tray *createtray (void *monitor, int height, int spacing, uint32_t *colorscheme,
+                  const char **fonts, const char *fontattrs, TrayNotifyCb cb,
+                  Watcher *watcher);
+void destroytray (Tray *tray);
+
+int tray_get_width (const Tray *tray);
+int tray_get_icon_width (const Tray *tray);
+void tray_update (Tray *tray);
+void tray_leftclicked (Tray *tray, unsigned int index);
+void tray_rightclicked (Tray *tray, unsigned int index, const char **menucmd);
+
+#endif /* TRAY_H */
diff --git a/systray/watcher.c b/systray/watcher.c
new file mode 100644
index 0000000..8dd84b9
--- /dev/null
+++ b/systray/watcher.c
@@ -0,0 +1,551 @@
+#include "watcher.h"
+
+#include "item.h"
+#include "tray.h"
+
+#include <dbus/dbus.h>
+#include <wayland-util.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+// IWYU pragma: no_include "dbus/dbus-protocol.h"
+// IWYU pragma: no_include "dbus/dbus-shared.h"
+
+static const char *const match_rule =
+	"type='signal',"
+	"interface='" DBUS_INTERFACE_DBUS
+	"',"
+	"member='NameOwnerChanged'";
+
+static const char *const snw_xml =
+	"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n"
+	" \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
+	"<node>\n"
+	"    <interface name=\"" DBUS_INTERFACE_PROPERTIES
+	"\">\n"
+	"        <method name=\"Get\">\n"
+	"            <arg type=\"s\" name=\"interface_name\" direction=\"in\"/>\n"
+	"            <arg type=\"s\" name=\"property_name\" direction=\"in\"/>\n"
+	"            <arg type=\"v\" name=\"value\" direction=\"out\"/>\n"
+	"        </method>\n"
+	"        <method name=\"GetAll\">\n"
+	"            <arg type=\"s\" name=\"interface_name\" direction=\"in\"/>\n"
+	"            <arg type=\"a{sv}\" name=\"properties\" direction=\"out\"/>\n"
+	"        </method>\n"
+	"        <method name=\"Set\">\n"
+	"            <arg type=\"s\" name=\"interface_name\" direction=\"in\"/>\n"
+	"            <arg type=\"s\" name=\"property_name\" direction=\"in\"/>\n"
+	"            <arg type=\"v\" name=\"value\" direction=\"in\"/>\n"
+	"        </method>\n"
+	"        <signal name=\"PropertiesChanged\">\n"
+	"            <arg type=\"s\" name=\"interface_name\"/>\n"
+	"            <arg type=\"a{sv}\" name=\"changed_properties\"/>\n"
+	"            <arg type=\"as\" name=\"invalidated_properties\"/>\n"
+	"        </signal>\n"
+	"    </interface>\n"
+	"    <interface name=\"" DBUS_INTERFACE_INTROSPECTABLE
+	"\">\n"
+	"        <method name=\"Introspect\">\n"
+	"            <arg type=\"s\" name=\"xml_data\" direction=\"out\"/>\n"
+	"        </method>\n"
+	"    </interface>\n"
+	"    <interface name=\"" DBUS_INTERFACE_PEER
+	"\">\n"
+	"        <method name=\"Ping\"/>\n"
+	"        <method name=\"GetMachineId\">\n"
+	"            <arg type=\"s\" name=\"machine_uuid\" direction=\"out\"/>\n"
+	"        </method>\n"
+	"    </interface>\n"
+	"    <interface name=\"" SNW_IFACE
+	"\">\n"
+	"        <!-- methods -->\n"
+	"        <method name=\"RegisterStatusNotifierItem\">\n"
+	"            <arg name=\"service\" type=\"s\" direction=\"in\" />\n"
+	"        </method>\n"
+	"        <!-- properties -->\n"
+	"        <property name=\"IsStatusNotifierHostRegistered\" type=\"b\" access=\"read\" />\n"
+	"        <property name=\"ProtocolVersion\" type=\"i\" access=\"read\" />\n"
+	"        <property name=\"RegisteredStatusNotifierItems\" type=\"as\" access=\"read\" />\n"
+	"        <!-- signals -->\n"
+	"        <signal name=\"StatusNotifierHostRegistered\">\n"
+	"        </signal>\n"
+	"    </interface>\n"
+	"</node>\n";
+
+static void
+unregister_item(Watcher *watcher, Item *item)
+{
+	wl_list_remove(&item->link);
+	destroyitem(item);
+
+	watcher_update_trays(watcher);
+}
+
+static Item *
+item_name_to_ptr(const Watcher *watcher, const char *busname)
+{
+	Item *item;
+
+	wl_list_for_each(item, &watcher->items, link) {
+		if (!item || !item->busname)
+			return NULL;
+		if (strcmp(item->busname, busname) == 0)
+			return item;
+	}
+
+	return NULL;
+}
+
+static DBusHandlerResult
+handle_nameowner_changed(Watcher *watcher, DBusConnection *conn,
+                         DBusMessage *msg)
+{
+	char *name, *old_owner, *new_owner;
+	Item *item;
+
+	if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &name,
+	                           DBUS_TYPE_STRING, &old_owner,
+	                           DBUS_TYPE_STRING, &new_owner,
+	                           DBUS_TYPE_INVALID)) {
+		return DBUS_HANDLER_RESULT_HANDLED;
+	}
+
+	if (*new_owner != '\0' || *name == '\0')
+		return DBUS_HANDLER_RESULT_HANDLED;
+
+	item = item_name_to_ptr(watcher, name);
+	if (!item)
+		return DBUS_HANDLER_RESULT_HANDLED;
+
+	unregister_item(watcher, item);
+
+	return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult
+filter_bus(DBusConnection *conn, DBusMessage *msg, void *data)
+{
+	Watcher *watcher = data;
+
+	if (dbus_message_is_signal(msg, DBUS_INTERFACE_DBUS,
+	                           "NameOwnerChanged"))
+		return handle_nameowner_changed(watcher, conn, msg);
+
+	else
+		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static DBusHandlerResult
+respond_register_item(Watcher *watcher, DBusConnection *conn, DBusMessage *msg)
+{
+	DBusHandlerResult res = DBUS_HANDLER_RESULT_HANDLED;
+
+	DBusMessage *reply = NULL;
+	Item *item;
+	const char *sender, *param, *busobj, *registree_name;
+
+	if (!(sender = dbus_message_get_sender(msg)) ||
+	    !dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &param,
+	                           DBUS_TYPE_INVALID)) {
+		reply = dbus_message_new_error(msg, DBUS_ERROR_INVALID_ARGS,
+		                               "Malformed message");
+		goto send;
+	}
+
+	switch (*param) {
+	case '/':
+		registree_name = sender;
+		busobj = param;
+		break;
+	case ':':
+		registree_name = param;
+		busobj = SNI_OPATH;
+		break;
+	default:
+		reply = dbus_message_new_error_printf(msg,
+		                                      DBUS_ERROR_INVALID_ARGS,
+		                                      "Bad argument: \"%s\"",
+		                                      param);
+		goto send;
+	}
+
+	if (*registree_name != ':' ||
+	    !dbus_validate_bus_name(registree_name, NULL)) {
+		reply = dbus_message_new_error_printf(msg,
+		                                      DBUS_ERROR_INVALID_ARGS,
+		                                      "Invalid busname %s",
+		                                      registree_name);
+		goto send;
+	}
+
+	if (item_name_to_ptr(watcher, registree_name)) {
+		reply = dbus_message_new_error_printf(msg,
+		                                      DBUS_ERROR_INVALID_ARGS,
+		                                      "%s already tracked",
+		                                      registree_name);
+		goto send;
+	}
+
+	item = createitem(registree_name, busobj, watcher);
+	wl_list_insert(&watcher->items, &item->link);
+	watcher_update_trays(watcher);
+
+	reply = dbus_message_new_method_return(msg);
+
+send:
+	if (!reply || !dbus_connection_send(conn, reply, NULL))
+		res = DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+	if (reply)
+		dbus_message_unref(reply);
+	return res;
+}
+
+static int
+get_registered_items(const Watcher *watcher, DBusMessageIter *iter)
+{
+	DBusMessageIter names = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	Item *item;
+	int r;
+
+	if (!dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY,
+	                                      DBUS_TYPE_STRING_AS_STRING,
+	                                      &names)) {
+		r = -ENOMEM;
+		goto fail;
+	}
+
+	wl_list_for_each(item, &watcher->items, link) {
+		if (!dbus_message_iter_append_basic(&names, DBUS_TYPE_STRING,
+		                                    &item->busname)) {
+			r = -ENOMEM;
+			goto fail;
+		}
+	}
+
+	dbus_message_iter_close_container(iter, &names);
+	return 0;
+
+fail:
+	dbus_message_iter_abandon_container_if_open(iter, &names);
+	return r;
+}
+
+static int
+get_registered_items_variant(const Watcher *watcher, DBusMessageIter *iter)
+{
+	DBusMessageIter variant = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	int r;
+
+	if (!dbus_message_iter_open_container(iter, DBUS_TYPE_VARIANT, "as",
+	                                      &variant) ||
+	    get_registered_items(watcher, &variant) < 0) {
+		r = -ENOMEM;
+		goto fail;
+	}
+
+	dbus_message_iter_close_container(iter, &variant);
+	return 0;
+
+fail:
+	dbus_message_iter_abandon_container_if_open(iter, &variant);
+	return r;
+}
+
+static int
+get_isregistered(DBusMessageIter *iter)
+{
+	DBusMessageIter variant = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	dbus_bool_t is_registered = TRUE;
+	int r;
+
+	if (!dbus_message_iter_open_container(iter, DBUS_TYPE_VARIANT,
+	                                      DBUS_TYPE_BOOLEAN_AS_STRING,
+	                                      &variant) ||
+	    !dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN,
+	                                    &is_registered)) {
+		r = -ENOMEM;
+		goto fail;
+	}
+
+	dbus_message_iter_close_container(iter, &variant);
+	return 0;
+
+fail:
+	dbus_message_iter_abandon_container_if_open(iter, &variant);
+	return r;
+}
+
+static int
+get_version(DBusMessageIter *iter)
+{
+	DBusMessageIter variant = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	dbus_int32_t protovers = 0;
+	int r;
+
+	if (!dbus_message_iter_open_container(iter, DBUS_TYPE_VARIANT,
+	                                      DBUS_TYPE_INT32_AS_STRING,
+	                                      &variant) ||
+	    !dbus_message_iter_append_basic(&variant, DBUS_TYPE_INT32,
+	                                    &protovers)) {
+		r = -ENOMEM;
+		goto fail;
+	}
+
+	dbus_message_iter_close_container(iter, &variant);
+	return 0;
+
+fail:
+	dbus_message_iter_abandon_container_if_open(iter, &variant);
+	return r;
+}
+
+static DBusHandlerResult
+respond_get_prop(Watcher *watcher, DBusConnection *conn, DBusMessage *msg)
+{
+	DBusError err = DBUS_ERROR_INIT;
+	DBusMessage *reply = NULL;
+	DBusMessageIter iter = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	const char *iface, *prop;
+
+	if (!dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &iface,
+	                           DBUS_TYPE_STRING, &prop,
+	                           DBUS_TYPE_INVALID)) {
+		reply = dbus_message_new_error(msg, err.name, err.message);
+		dbus_error_free(&err);
+		goto send;
+	}
+
+	if (strcmp(iface, SNW_IFACE) != 0) {
+		reply = dbus_message_new_error_printf(
+			msg, DBUS_ERROR_UNKNOWN_INTERFACE,
+			"Unknown interface \"%s\"", iface);
+		goto send;
+	}
+
+	reply = dbus_message_new_method_return(msg);
+	if (!reply)
+		goto fail;
+
+	if (strcmp(prop, "ProtocolVersion") == 0) {
+		dbus_message_iter_init_append(reply, &iter);
+		if (get_version(&iter) < 0)
+			goto fail;
+
+	} else if (strcmp(prop, "IsStatusNotifierHostRegistered") == 0) {
+		dbus_message_iter_init_append(reply, &iter);
+		if (get_isregistered(&iter) < 0)
+			goto fail;
+
+	} else if (strcmp(prop, "RegisteredStatusNotifierItems") == 0) {
+		dbus_message_iter_init_append(reply, &iter);
+		if (get_registered_items_variant(watcher, &iter) < 0)
+			goto fail;
+
+	} else {
+		dbus_message_unref(reply);
+		reply = dbus_message_new_error_printf(
+			reply, DBUS_ERROR_UNKNOWN_PROPERTY,
+			"Property \"%s\" does not exist", prop);
+	}
+
+send:
+	if (!reply || !dbus_connection_send(conn, reply, NULL))
+		goto fail;
+
+	if (reply)
+		dbus_message_unref(reply);
+	return DBUS_HANDLER_RESULT_HANDLED;
+
+fail:
+	if (reply)
+		dbus_message_unref(reply);
+	return DBUS_HANDLER_RESULT_NEED_MEMORY;
+}
+
+static DBusHandlerResult
+respond_all_props(Watcher *watcher, DBusConnection *conn, DBusMessage *msg)
+{
+	DBusMessage *reply = NULL;
+	DBusMessageIter array = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	DBusMessageIter dict = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	DBusMessageIter iter = DBUS_MESSAGE_ITER_INIT_CLOSED;
+	const char *prop;
+
+	reply = dbus_message_new_method_return(msg);
+	if (!reply)
+		goto fail;
+	dbus_message_iter_init_append(reply, &iter);
+
+	if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
+	                                      &array))
+		goto fail;
+
+	prop = "ProtocolVersion";
+	if (!dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY,
+	                                      NULL, &dict) ||
+	    !dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &prop) ||
+	    get_version(&dict) < 0 ||
+	    !dbus_message_iter_close_container(&array, &dict)) {
+		goto fail;
+	}
+
+	prop = "IsStatusNotifierHostRegistered";
+	if (!dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY,
+	                                      NULL, &dict) ||
+	    !dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &prop) ||
+	    get_isregistered(&dict) < 0 ||
+	    !dbus_message_iter_close_container(&array, &dict)) {
+		goto fail;
+	}
+
+	prop = "RegisteredStatusNotifierItems";
+	if (!dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY,
+	                                      NULL, &dict) ||
+	    !dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &prop) ||
+	    get_registered_items_variant(watcher, &dict) < 0 ||
+	    !dbus_message_iter_close_container(&array, &dict)) {
+		goto fail;
+	}
+
+	if (!dbus_message_iter_close_container(&iter, &array) ||
+	    !dbus_connection_send(conn, reply, NULL)) {
+		goto fail;
+	}
+
+	dbus_message_unref(reply);
+	return DBUS_HANDLER_RESULT_HANDLED;
+
+fail:
+	dbus_message_iter_abandon_container_if_open(&array, &dict);
+	dbus_message_iter_abandon_container_if_open(&iter, &array);
+	if (reply)
+		dbus_message_unref(reply);
+	return DBUS_HANDLER_RESULT_NEED_MEMORY;
+}
+
+static DBusHandlerResult
+respond_introspect(DBusConnection *conn, DBusMessage *msg)
+{
+	DBusMessage *reply = NULL;
+
+	reply = dbus_message_new_method_return(msg);
+	if (!reply)
+		goto fail;
+
+	if (!dbus_message_append_args(reply, DBUS_TYPE_STRING, &snw_xml,
+	                              DBUS_TYPE_INVALID) ||
+	    !dbus_connection_send(conn, reply, NULL)) {
+		goto fail;
+	}
+
+	dbus_message_unref(reply);
+	return DBUS_HANDLER_RESULT_HANDLED;
+
+fail:
+	if (reply)
+		dbus_message_unref(reply);
+	return DBUS_HANDLER_RESULT_NEED_MEMORY;
+}
+
+static DBusHandlerResult
+snw_message_handler(DBusConnection *conn, DBusMessage *msg, void *data)
+{
+	Watcher *watcher = data;
+
+	if (dbus_message_is_method_call(msg, DBUS_INTERFACE_INTROSPECTABLE,
+	                                "Introspect"))
+		return respond_introspect(conn, msg);
+
+	else if (dbus_message_is_method_call(msg, DBUS_INTERFACE_PROPERTIES,
+	                                     "GetAll"))
+		return respond_all_props(watcher, conn, msg);
+
+	else if (dbus_message_is_method_call(msg, DBUS_INTERFACE_PROPERTIES,
+	                                     "Get"))
+		return respond_get_prop(watcher, conn, msg);
+
+	else if (dbus_message_is_method_call(msg, SNW_IFACE,
+	                                     "RegisterStatusNotifierItem"))
+		return respond_register_item(watcher, conn, msg);
+
+	else
+		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static const DBusObjectPathVTable snw_vtable = { .message_function =
+	                                                 snw_message_handler };
+
+void
+watcher_start(Watcher *watcher, DBusConnection *conn,
+              struct wl_event_loop *loop)
+{
+	DBusError err = DBUS_ERROR_INIT;
+	int r, flags;
+
+	wl_list_init(&watcher->items);
+	wl_list_init(&watcher->trays);
+	watcher->conn = conn;
+	watcher->loop = loop;
+
+	flags = DBUS_NAME_FLAG_REPLACE_EXISTING | DBUS_NAME_FLAG_DO_NOT_QUEUE;
+	r = dbus_bus_request_name(conn, SNW_NAME,
+	                          flags, NULL);
+	if (r != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER)
+		goto fail;
+
+	if (!dbus_connection_add_filter(conn, filter_bus, watcher, NULL)) {
+		dbus_bus_release_name(conn, SNW_NAME, NULL);
+		goto fail;
+	}
+
+	dbus_bus_add_match(conn, match_rule, &err);
+	if (dbus_error_is_set(&err)) {
+		dbus_connection_remove_filter(conn, filter_bus, watcher);
+		dbus_bus_release_name(conn, SNW_NAME, NULL);
+		goto fail;
+	}
+
+	if (!dbus_connection_register_object_path(conn, SNW_OPATH, &snw_vtable,
+	                                          watcher)) {
+		dbus_bus_remove_match(conn, match_rule, NULL);
+		dbus_connection_remove_filter(conn, filter_bus, watcher);
+		dbus_bus_release_name(conn, SNW_NAME, NULL);
+		goto fail;
+	}
+
+	watcher->running = 1;
+	return;
+
+fail:
+	fprintf(stderr, "Couldn't start watcher, systray not available\n");
+	dbus_error_free(&err);
+	return;
+}
+
+void
+watcher_stop(Watcher *watcher)
+{
+	dbus_connection_unregister_object_path(watcher->conn, SNW_OPATH);
+	dbus_bus_remove_match(watcher->conn, match_rule, NULL);
+	dbus_connection_remove_filter(watcher->conn, filter_bus, watcher);
+	dbus_bus_release_name(watcher->conn, SNW_NAME, NULL);
+	watcher->running = 0;
+}
+
+int
+watcher_get_n_items(const Watcher *watcher)
+{
+	return wl_list_length(&watcher->items);
+}
+
+void
+watcher_update_trays(Watcher *watcher)
+{
+	Tray *tray;
+
+	wl_list_for_each(tray, &watcher->trays, link)
+		tray_update(tray);
+}
diff --git a/systray/watcher.h b/systray/watcher.h
new file mode 100644
index 0000000..127eb64
--- /dev/null
+++ b/systray/watcher.h
@@ -0,0 +1,35 @@
+#ifndef WATCHER_H
+#define WATCHER_H
+
+#include <dbus/dbus.h>
+#include <wayland-server-core.h>
+#include <wayland-util.h>
+
+/*
+ * The FDO spec says "org.freedesktop.StatusNotifierWatcher"[1],
+ * but both the client libraries[2,3] actually use "org.kde.StatusNotifierWatcher"
+ *
+ * [1] https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/
+ * [2] https://github.com/AyatanaIndicators/libayatana-appindicator-glib
+ * [3] https://invent.kde.org/frameworks/kstatusnotifieritem
+ */
+#define SNW_NAME "org.kde.StatusNotifierWatcher"
+#define SNW_OPATH "/StatusNotifierWatcher"
+#define SNW_IFACE "org.kde.StatusNotifierWatcher"
+
+typedef struct {
+	struct wl_list items;
+	struct wl_list trays;
+	struct wl_event_loop *loop;
+	DBusConnection *conn;
+	int running;
+} Watcher;
+
+void watcher_start (Watcher *watcher, DBusConnection *conn,
+                   struct wl_event_loop *loop);
+void watcher_stop (Watcher *watcher);
+
+int watcher_get_n_items (const Watcher *watcher);
+void watcher_update_trays (Watcher *watcher);
+
+#endif /* WATCHER_H */
-- 
2.49.0

