dwl-patches/patches/bar-systray/bar-systray-0.7.patch
vetu104 7d54a01970 bar-systray: Support non-linux
- Remove unportable code: use self-pipe for waking up the wl_event_loop
  on dbus events instead of eventfd. Tested on FreeBSD.

- Don't die if another tray is already running. Previous version didn't
  allow nested dwls.
2025-03-29 19:26:20 +02:00

3024 lines
78 KiB
Diff

From e40de8cb1f33ebd7978f7f7843aa94ee241cb55a 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..8a13181
--- /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