summaryrefslogtreecommitdiffstats
path: root/dwl-patches/patches/bar-systray/bar-systray-0.7.patch
diff options
context:
space:
mode:
Diffstat (limited to 'dwl-patches/patches/bar-systray/bar-systray-0.7.patch')
-rw-r--r--dwl-patches/patches/bar-systray/bar-systray-0.7.patch3023
1 files changed, 3023 insertions, 0 deletions
diff --git a/dwl-patches/patches/bar-systray/bar-systray-0.7.patch b/dwl-patches/patches/bar-systray/bar-systray-0.7.patch
new file mode 100644
index 0000000..1dda4c6
--- /dev/null
+++ b/dwl-patches/patches/bar-systray/bar-systray-0.7.patch
@@ -0,0 +1,3023 @@
+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
+