aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile23
-rw-r--r--config.def.h17
-rw-r--r--dbus.c240
-rw-r--r--dbus.h10
-rw-r--r--drwl.h311
-rw-r--r--dwl.c109
-rw-r--r--item.c403
-rw-r--r--item.h46
-rw-r--r--systray/helpers.c43
-rw-r--r--systray/helpers.h12
-rw-r--r--systray/icon.c149
-rw-r--r--systray/icon.h26
-rw-r--r--systray/menu.c757
-rw-r--r--systray/menu.h11
-rw-r--r--systray/tray.c237
-rw-r--r--systray/tray.h37
-rw-r--r--systray/watcher.c549
-rw-r--r--systray/watcher.h34
18 files changed, 2995 insertions, 19 deletions
diff --git a/Makefile b/Makefile
index 279b1c0..6251a2a 100644
--- a/Makefile
+++ b/Makefile
@@ -12,17 +12,28 @@ DWLDEVCFLAGS = -g -Wpedantic -Wall -Wextra -Wdeclaration-after-statement \
-Wfloat-conversion
# CFLAGS / LDFLAGS
-PKGS = wayland-server xkbcommon libinput pixman-1 fcft $(XLIBS)
+PKGS = wayland-server xkbcommon libinput pixman-1 fcft $(XLIBS) dbus-1
DWLCFLAGS = `$(PKG_CONFIG) --cflags $(PKGS)` $(WLR_INCS) $(DWLCPPFLAGS) $(DWLDEVCFLAGS) $(CFLAGS)
LDLIBS = `$(PKG_CONFIG) --libs $(PKGS)` $(WLR_LIBS) -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 $(TRAYOJBS) $(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 8d022de..af1f935 100644
--- a/config.def.h
+++ b/config.def.h
@@ -7,9 +7,11 @@
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"};
+static const char *fonts[] = {"HackNerdFont:size=16"};
static const float rootcolor[] = COLOR(0x000000ff);
/* This conforms to the xdg-protocol. Set the alpha to zero to restore the old behavior */
static const float fullscreen_bg[] = {0.1f, 0.1f, 0.1f, 1.0f}; /* You can also use glsl colors */
@@ -50,11 +52,9 @@ static const Layout layouts[] = {
/* NOTE: ALWAYS add a fallback rule, even if you are completely sure it won't be used */
static const MonitorRule monrules[] = {
/* name mfact nmaster scale layout rotate/reflect x y */
- /* example of a HiDPI laptop monitor:
- { "eDP-1", 0.5f, 1, 2, &layouts[0], WL_OUTPUT_TRANSFORM_NORMAL, -1, -1 },
- */
+ { "eDP-1", 0.5f, 1, 1.5, &layouts[0], WL_OUTPUT_TRANSFORM_NORMAL, -1, -1 },
/* defaults */
- { NULL, 0.55f, 1, 1, &layouts[0], WL_OUTPUT_TRANSFORM_NORMAL, -1, -1 },
+ { NULL, 0.5f, 1, 1, &layouts[0], WL_OUTPUT_TRANSFORM_NORMAL, -1, -1 },
};
/* keyboard */
@@ -66,8 +66,8 @@ static const struct xkb_rule_names xkb_rules = {
.options = NULL,
};
-static const int repeat_rate = 25;
-static const int repeat_delay = 600;
+static const int repeat_rate = 50;
+static const int repeat_delay = 300;
/* Trackpad */
static const int tap_to_click = 1;
@@ -127,6 +127,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 +189,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..653a133
--- /dev/null
+++ b/dbus.c
@@ -0,0 +1,240 @@
+#include "dbus.h"
+
+#include <dbus/dbus.h>
+#include <wayland-server-core.h>
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#if defined __linux__
+#include <sys/eventfd.h>
+#elif defined(__FreeBSD__) || defined(__OpenBSD__)
+#include <sys/event.h>
+#endif
+#include <unistd.h>
+
+int efd = -1;
+
+static int
+dwl_dbus_dispatch(int fd, unsigned int mask, void *data)
+{
+ DBusConnection *conn = data;
+
+ uint64_t dispatch_pending;
+ DBusDispatchStatus status;
+
+ status = dbus_connection_dispatch(conn);
+
+ /*
+ * Don't clear pending flag if message queue wasn't
+ * fully drained
+ */
+ if (status != DBUS_DISPATCH_COMPLETE)
+ return 0;
+
+ if (read(fd, &dispatch_pending, sizeof(uint64_t)) < 0)
+ perror("read");
+
+ 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_adjust_timeout(DBusTimeout *timeout, void *data)
+{
+ int interval;
+ struct wl_event_source *timeout_source;
+
+ timeout_source = dbus_timeout_get_data(timeout);
+
+ if (timeout_source) {
+ interval = dbus_timeout_get_interval(timeout);
+ wl_event_source_timer_update(timeout_source, interval);
+ }
+}
+
+static void
+dwl_dbus_dispatch_status(DBusConnection *conn, DBusDispatchStatus status, void *data)
+{
+ if (status == DBUS_DISPATCH_DATA_REMAINS) {
+ uint64_t dispatch_pending = 1;
+ if (write(efd, &dispatch_pending, sizeof(uint64_t)) < 0)
+ perror("write");
+ }
+}
+
+struct wl_event_source *
+startbus(DBusConnection *conn, struct wl_event_loop *loop)
+{
+ struct wl_event_source *bus_source = NULL;
+ uint64_t dispatch_pending = 1;
+
+ dbus_connection_set_exit_on_disconnect(conn, FALSE);
+
+#if defined __linux__
+ efd = eventfd(0, EFD_CLOEXEC);
+#elif defined(__FreeBSD__) || defined(__OpenBSD__)
+ efd = kqueue();
+#endif
+ if (efd < 0)
+ goto fail;
+
+ dbus_connection_set_dispatch_status_function(conn, dwl_dbus_dispatch_status, NULL, NULL);
+
+ 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,
+ dwl_dbus_adjust_timeout, loop, NULL)) {
+ goto fail;
+ }
+
+ bus_source = wl_event_loop_add_fd(loop, efd, WL_EVENT_READABLE, dwl_dbus_dispatch, conn);
+ if (!bus_source)
+ goto fail;
+
+ if (dbus_connection_get_dispatch_status(conn) == DBUS_DISPATCH_DATA_REMAINS)
+ if (write(efd, &dispatch_pending, sizeof(uint64_t)) < 0)
+ perror("write");
+
+ return bus_source;
+
+fail:
+ if (bus_source)
+ wl_event_source_remove(bus_source);
+ if (efd >= 0) {
+ close(efd);
+ efd = -1;
+ }
+ 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);
+ close(efd);
+ efd = -1;
+
+ 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/drwl.h b/drwl.h
new file mode 100644
index 0000000..b06a736
--- /dev/null
+++ b/drwl.h
@@ -0,0 +1,311 @@
+/*
+ * drwl - https://codeberg.org/sewn/drwl
+ *
+ * Copyright (c) 2023-2024 sewn <sewn@disroot.org>
+ * Copyright (c) 2024 notchoc <notchoc@disroot.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * The UTF-8 Decoder included is from Bjoern Hoehrmann:
+ * Copyright (c) 2008-2010 Bjoern Hoehrmann <bjoern@hoehrmann.de>
+ * See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.
+ */
+#pragma once
+
+#include <stdlib.h>
+#include <fcft/fcft.h>
+#include <pixman-1/pixman.h>
+
+enum { ColFg, ColBg, ColBorder }; /* colorscheme index */
+
+typedef struct fcft_font Fnt;
+typedef pixman_image_t Img;
+
+typedef struct {
+ Img *image;
+ Fnt *font;
+ uint32_t *scheme;
+} Drwl;
+
+#define UTF8_ACCEPT 0
+#define UTF8_REJECT 12
+#define UTF8_INVALID 0xFFFD
+
+static const uint8_t utf8d[] = {
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
+ 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
+ 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+ 10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8,
+
+ 0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,
+ 12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,
+ 12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,
+ 12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,
+ 12,36,12,12,12,12,12,12,12,12,12,12,
+};
+
+static inline uint32_t
+utf8decode(uint32_t *state, uint32_t *codep, uint8_t byte)
+{
+ uint32_t type = utf8d[byte];
+
+ *codep = (*state != UTF8_ACCEPT) ?
+ (byte & 0x3fu) | (*codep << 6) :
+ (0xff >> type) & (byte);
+
+ *state = utf8d[256 + *state + type];
+ return *state;
+}
+
+static int
+drwl_init(void)
+{
+ fcft_set_scaling_filter(FCFT_SCALING_FILTER_LANCZOS3);
+ return fcft_init(FCFT_LOG_COLORIZE_AUTO, 0, FCFT_LOG_CLASS_ERROR);
+}
+
+static Drwl *
+drwl_create(void)
+{
+ Drwl *drwl;
+
+ if (!(drwl = calloc(1, sizeof(Drwl))))
+ return NULL;
+
+ return drwl;
+}
+
+static void
+drwl_setfont(Drwl *drwl, Fnt *font)
+{
+ if (drwl)
+ drwl->font = font;
+}
+
+static void
+drwl_setimage(Drwl *drwl, Img *image)
+{
+ if (drwl)
+ drwl->image = image;
+}
+
+static Fnt *
+drwl_font_create(Drwl *drwl, size_t count,
+ const char *names[static count], const char *attributes)
+{
+ Fnt *font = fcft_from_name(count, names, attributes);
+ if (drwl)
+ drwl_setfont(drwl, font);
+ return font;
+}
+
+static void
+drwl_font_destroy(Fnt *font)
+{
+ fcft_destroy(font);
+}
+
+static inline pixman_color_t
+convert_color(uint32_t clr)
+{
+ return (pixman_color_t){
+ ((clr >> 24) & 0xFF) * 0x101 * (clr & 0xFF) / 0xFF,
+ ((clr >> 16) & 0xFF) * 0x101 * (clr & 0xFF) / 0xFF,
+ ((clr >> 8) & 0xFF) * 0x101 * (clr & 0xFF) / 0xFF,
+ (clr & 0xFF) * 0x101
+ };
+}
+
+static void
+drwl_setscheme(Drwl *drwl, uint32_t *scm)
+{
+ if (drwl)
+ drwl->scheme = scm;
+}
+
+static Img *
+drwl_image_create(Drwl *drwl, unsigned int w, unsigned int h, uint32_t *bits)
+{
+ Img *image;
+ pixman_region32_t clip;
+
+ image = pixman_image_create_bits_no_clear(
+ PIXMAN_a8r8g8b8, w, h, bits, w * 4);
+ if (!image)
+ return NULL;
+ pixman_region32_init_rect(&clip, 0, 0, w, h);
+ pixman_image_set_clip_region32(image, &clip);
+ pixman_region32_fini(&clip);
+
+ if (drwl)
+ drwl_setimage(drwl, image);
+ return image;
+}
+
+static void
+drwl_rect(Drwl *drwl,
+ int x, int y, unsigned int w, unsigned int h,
+ int filled, int invert)
+{
+ pixman_color_t clr;
+ if (!drwl || !drwl->scheme || !drwl->image)
+ return;
+
+ clr = convert_color(drwl->scheme[invert ? ColBg : ColFg]);
+ if (filled)
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, drwl->image, &clr, 1,
+ &(pixman_rectangle16_t){x, y, w, h});
+ else
+ pixman_image_fill_rectangles(PIXMAN_OP_SRC, drwl->image, &clr, 4,
+ (pixman_rectangle16_t[4]){
+ { x, y, w, 1 },
+ { x, y + h - 1, w, 1 },
+ { x, y, 1, h },
+ { x + w - 1, y, 1, h }});
+}
+
+static int
+drwl_text(Drwl *drwl,
+ int x, int y, unsigned int w, unsigned int h,
+ unsigned int lpad, const char *text, int invert)
+{
+ int ty;
+ int render = x || y || w || h;
+ long x_kern;
+ uint32_t cp = 0, last_cp = 0, state;
+ pixman_color_t clr;
+ pixman_image_t *fg_pix = NULL;
+ int noellipsis = 0;
+ const struct fcft_glyph *glyph, *eg = NULL;
+ int fcft_subpixel_mode = FCFT_SUBPIXEL_DEFAULT;
+
+ if (!drwl || (render && (!drwl->scheme || !w || !drwl->image)) || !text || !drwl->font)
+ return 0;
+
+ if (!render) {
+ w = invert ? invert : ~invert;
+ } else {
+ clr = convert_color(drwl->scheme[invert ? ColBg : ColFg]);
+ fg_pix = pixman_image_create_solid_fill(&clr);
+
+ drwl_rect(drwl, x, y, w, h, 1, !invert);
+
+ x += lpad;
+ w -= lpad;
+ }
+
+ if (render && (drwl->scheme[ColBg] & 0xFF) != 0xFF)
+ fcft_subpixel_mode = FCFT_SUBPIXEL_NONE;
+
+ if (render)
+ eg = fcft_rasterize_char_utf32(drwl->font, 0x2026 /* … */, fcft_subpixel_mode);
+
+ for (const char *p = text, *pp; pp = p, *p; p++) {
+ for (state = UTF8_ACCEPT; *p &&
+ utf8decode(&state, &cp, *p) > UTF8_REJECT; p++)
+ ;
+ if (!*p || state == UTF8_REJECT) {
+ cp = UTF8_INVALID;
+ if (p > pp)
+ p--;
+ }
+
+ glyph = fcft_rasterize_char_utf32(drwl->font, cp, fcft_subpixel_mode);
+ if (!glyph)
+ continue;
+
+ x_kern = 0;
+ if (last_cp)
+ fcft_kerning(drwl->font, last_cp, cp, &x_kern, NULL);
+ last_cp = cp;
+
+ ty = y + (h - drwl->font->height) / 2 + drwl->font->ascent;
+
+ if (render && !noellipsis && x_kern + glyph->advance.x + eg->advance.x > w &&
+ *(p + 1) != '\0') {
+ /* cannot fit ellipsis after current codepoint */
+ if (drwl_text(drwl, 0, 0, 0, 0, 0, pp, 0) + x_kern <= w) {
+ noellipsis = 1;
+ } else {
+ w -= eg->advance.x;
+ pixman_image_composite32(
+ PIXMAN_OP_OVER, fg_pix, eg->pix, drwl->image, 0, 0, 0, 0,
+ x + eg->x, ty - eg->y, eg->width, eg->height);
+ }
+ }
+
+ if ((x_kern + glyph->advance.x) > w)
+ break;
+
+ x += x_kern;
+
+ if (render && pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)
+ /* pre-rendered glyphs (eg. emoji) */
+ pixman_image_composite32(
+ PIXMAN_OP_OVER, glyph->pix, NULL, drwl->image, 0, 0, 0, 0,
+ x + glyph->x, ty - glyph->y, glyph->width, glyph->height);
+ else if (render)
+ pixman_image_composite32(
+ PIXMAN_OP_OVER, fg_pix, glyph->pix, drwl->image, 0, 0, 0, 0,
+ x + glyph->x, ty - glyph->y, glyph->width, glyph->height);
+
+ x += glyph->advance.x;
+ w -= glyph->advance.x;
+ }
+
+ if (render)
+ pixman_image_unref(fg_pix);
+
+ return x + (render ? w : 0);
+}
+
+static unsigned int
+drwl_font_getwidth(Drwl *drwl, const char *text)
+{
+ if (!drwl || !drwl->font || !text)
+ return 0;
+ return drwl_text(drwl, 0, 0, 0, 0, 0, text, 0);
+}
+
+static void
+drwl_image_destroy(Img *image)
+{
+ pixman_image_unref(image);
+}
+
+static void
+drwl_destroy(Drwl *drwl)
+{
+ if (drwl->font)
+ drwl_font_destroy(drwl->font);
+ if (drwl->image)
+ drwl_image_destroy(drwl->image);
+ free(drwl);
+}
+
+static void
+drwl_fini(void)
+{
+ fcft_fini();
+}
diff --git a/dwl.c b/dwl.c
index b28b3a7..e3693aa 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>
@@ -72,6 +73,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))
@@ -86,9 +90,11 @@
#define TEXTW(mon, text) (drwl_font_getwidth(mon->drw, text) + mon->lrpad)
/* enums */
+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, ClkTray }; /* clicks */
typedef union {
int i;
@@ -213,6 +219,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];
@@ -370,6 +377,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);
@@ -472,6 +482,10 @@ static struct wl_listener new_session_lock = {.notify = locksession};
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;
+
static const struct wlr_buffer_impl buffer_impl = {
.destroy = bufdestroy,
.begin_data_ptr_access = bufdatabegin,
@@ -738,8 +752,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;
@@ -749,6 +763,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);
@@ -767,17 +782,29 @@ buttonpress(struct wl_listener *listener, void *data)
if (!c && !exclusive_focus &&
(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));
+
if (i < LENGTH(tags)) {
click = ClkTagBar;
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;
}
@@ -791,7 +818,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;
}
}
@@ -858,6 +890,12 @@ cleanup(void)
destroykeyboardgroup(&kb_group->destroy, NULL);
+ if (showbar && showsystray) {
+ watcher_stop(&watcher);
+ 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);
@@ -886,6 +924,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);
@@ -1561,6 +1602,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;
@@ -1573,11 +1615,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) {
@@ -1603,7 +1647,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);
@@ -1615,6 +1659,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,
@@ -1624,6 +1677,26 @@ drawbar(Monitor *m)
}
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)
{
Monitor *m = NULL;
@@ -2831,6 +2904,17 @@ setup(void)
status_event_source = wl_event_loop_add_fd(wl_display_get_event_loop(dpy),
STDIN_FILENO, WL_EVENT_READABLE, statusin, NULL);
+ if (showbar && showsystray) {
+ 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 (watcher_start(&watcher, bus_conn, event_loop) < 0)
+ die("Failed to start tray watcher");
+ }
+
/* 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 */
@@ -3173,6 +3257,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;
@@ -3198,6 +3283,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/item.c b/item.c
new file mode 100644
index 0000000..8a13181
--- /dev/null
+++ b/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/item.h b/item.h
new file mode 100644
index 0000000..dc22e25
--- /dev/null
+++ b/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/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/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..072ab86
--- /dev/null
+++ b/systray/watcher.c
@@ -0,0 +1,549 @@
+#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 };
+
+int
+watcher_start(Watcher *watcher, DBusConnection *conn,
+ struct wl_event_loop *loop)
+{
+ DBusError err = DBUS_ERROR_INIT;
+ int r;
+
+ wl_list_init(&watcher->items);
+ wl_list_init(&watcher->trays);
+ watcher->conn = conn;
+ watcher->loop = loop;
+
+ r = dbus_bus_request_name(conn, SNW_NAME,
+ DBUS_NAME_FLAG_REPLACE_EXISTING, 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;
+ }
+
+ dbus_error_free(&err);
+ return 0;
+
+fail:
+ fprintf(stderr, "Couldn't start watcher, systray not available\n");
+ dbus_error_free(&err);
+ return -1;
+}
+
+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);
+}
+
+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..0178587
--- /dev/null
+++ b/systray/watcher.h
@@ -0,0 +1,34 @@
+#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;
+} Watcher;
+
+int 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 */