From a20f21281f5e25702ae93960d01fb27085c36d4c Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Mon, 2 Apr 2018 19:41:21 -0600 Subject: [PATCH] Port swaybar tray files to 0.15 and fix compiling --- include/swaybar/bar.h | 4 +- include/swaybar/config.h | 6 + include/swaybar/tray/dbus.h | 31 ++ include/swaybar/tray/icon.h | 2 +- include/swaybar/tray/sni.h | 23 +- include/swaybar/tray/tray.h | 16 +- meson.build | 7 + meson_options.txt | 1 + swaybar/bar.c | 11 + swaybar/meson.build | 34 +- swaybar/tray/dbus.c | 305 +++++++++++++++++ swaybar/tray/icon.c | 452 +++++++++++++++++++++++++ swaybar/tray/sni.c | 342 +++++++++++++++++++ swaybar/tray/sni_watcher.c | 638 ++++++++++++++++++++++++++++++++++++ swaybar/tray/tray.c | 516 +++++++++++++++++++++++++++++ 15 files changed, 2354 insertions(+), 34 deletions(-) create mode 100644 swaybar/tray/dbus.c create mode 100644 swaybar/tray/icon.c create mode 100644 swaybar/tray/sni.c create mode 100644 swaybar/tray/sni_watcher.c create mode 100644 swaybar/tray/tray.c diff --git a/include/swaybar/bar.h b/include/swaybar/bar.h index 74292519f..10092721f 100644 --- a/include/swaybar/bar.h +++ b/include/swaybar/bar.h @@ -49,7 +49,9 @@ struct swaybar_output { struct wl_output *output; struct wl_surface *surface; struct zwlr_layer_surface_v1 *layer_surface; - +#ifdef ENABLE_TRAY + struct wl_list item_refs; +#endif struct wl_list workspaces; struct wl_list hotspots; diff --git a/include/swaybar/config.h b/include/swaybar/config.h index 7f321df89..09b53f33b 100644 --- a/include/swaybar/config.h +++ b/include/swaybar/config.h @@ -33,6 +33,12 @@ struct swaybar_config { bool all_outputs; int height; +#ifdef ENABLE_TRAY + // TODO TRAY + // output, render, buttons + uint32_t tray_padding; +#endif + struct { uint32_t background; uint32_t statusline; diff --git a/include/swaybar/tray/dbus.h b/include/swaybar/tray/dbus.h index eb9cfea70..c693e6f76 100644 --- a/include/swaybar/tray/dbus.h +++ b/include/swaybar/tray/dbus.h @@ -5,6 +5,37 @@ #include extern DBusConnection *conn; +enum property_status { + PROP_EXISTS, /* Will give iter */ + PROP_ERROR, /* Will not give iter */ + PROP_BAD_DATA, /* Will not give iter */ + PROP_WRONG_SIG, /* Will give iter, please be careful */ +}; + +/** + * Checks the signature of the given iter against `sig`. Prefer to + * `dbus_message_iter_get_signature` as this one frees the intermediate string. + */ +bool dbus_message_iter_check_signature(DBusMessageIter *iter, const char *sig); + +/** + * Fetches the property and calls `callback` with a message iter pointing it. + * Performs error handling and signature checking. + * + * Returns: true if message is successfully sent and false otherwise. If there + * is an error getting a property, `callback` will still be run, but with + * `status` set to the error. + * + * NOTE: `expected_signature` must remain valid until the message reply is + * received, please only use 'static signatures. + */ +bool dbus_get_prop_async(const char *destination, + const char *path, + const char *iface, + const char *prop, + const char *expected_signature, + void(*callback)(DBusMessageIter *iter, void *data, enum property_status status), + void *data); /** * Should be called in main loop to dispatch events */ diff --git a/include/swaybar/tray/icon.h b/include/swaybar/tray/icon.h index 1cc6ff9c1..8bd5fabaf 100644 --- a/include/swaybar/tray/icon.h +++ b/include/swaybar/tray/icon.h @@ -3,7 +3,7 @@ #include #include -#include +#include "cairo.h" /** * Returns the image found by `name` that is closest to `size` diff --git a/include/swaybar/tray/sni.h b/include/swaybar/tray/sni.h index c2544e2a9..2150cebaa 100644 --- a/include/swaybar/tray/sni.h +++ b/include/swaybar/tray/sni.h @@ -2,13 +2,17 @@ #define _SWAYBAR_SNI_H #include -#include +#include +#include "cairo.h" struct StatusNotifierItem { + struct wl_list link; /* Name registered to sni watcher */ char *name; /* Unique bus name, needed for determining signal origins */ char *unique_name; + /* Object path, useful for items not registerd by well known name */ + char *object_path; bool kde_special_snowflake; cairo_surface_t *image; @@ -17,6 +21,7 @@ struct StatusNotifierItem { /* Each output holds an sni_icon_ref of each item to render */ struct sni_icon_ref { + struct wl_list link; cairo_surface_t *icon; struct StatusNotifierItem *ref; }; @@ -31,20 +36,12 @@ void sni_icon_ref_free(struct sni_icon_ref *sni_ref); * May return `NULL` if `name` is not valid. */ struct StatusNotifierItem *sni_create(const char *name); - /** - * `item` must be a struct StatusNotifierItem * - * `str` must be a NUL terminated char * - * - * Returns 0 if `item` has a name of `str` + * Same as sni_create, but takes an object path and unique name instead of + * well-known name. */ -int sni_str_cmp(const void *item, const void *str); - -/** - * Returns 0 if `item` has a unique name of `str` or if - * `item->unique_name == NULL` - */ -int sni_uniq_cmp(const void *item, const void *str); +struct StatusNotifierItem *sni_create_from_obj_path(const char *unique_name, + const char *object_path); /** * Gets an icon for the given item if found. diff --git a/include/swaybar/tray/tray.h b/include/swaybar/tray/tray.h index 2d0662bed..7acdf0a2e 100644 --- a/include/swaybar/tray/tray.h +++ b/include/swaybar/tray/tray.h @@ -8,25 +8,25 @@ #include "swaybar/bar.h" #include "list.h" -extern struct tray *tray; - struct tray { - list_t *items; + struct swaybar *bar; + struct wl_list items; }; /** * Processes a mouse event on the bar - */ + * TODO TRAY mouse void tray_mouse_event(struct output *output, int x, int y, uint32_t button, uint32_t state); +*/ -uint32_t tray_render(struct output *output, struct config *config); - -void tray_upkeep(struct bar *bar); +uint32_t render_tray(cairo_t *cairo, struct swaybar_output *output, + struct swaybar_config *config, struct swaybar_workspace *ws, + double *pos, uint32_t height); /** * Initializes the tray with D-Bus */ -void init_tray(struct bar *bar); +void init_tray(struct swaybar *bar); #endif /* _SWAYBAR_TRAY_H */ diff --git a/meson.build b/meson.build index 01788fd9f..18616d9ca 100644 --- a/meson.build +++ b/meson.build @@ -19,6 +19,8 @@ datadir = get_option('datadir') sysconfdir = get_option('sysconfdir') prefix = get_option('prefix') +enable_tray = get_option('enable_tray') + jsonc = dependency('json-c', version: '>=0.13') pcre = dependency('libpcre') wlroots = dependency('wlroots', fallback: ['wlroots', 'wlroots']) @@ -35,6 +37,7 @@ gdk_pixbuf = dependency('gdk-pixbuf-2.0', required: false) pixman = dependency('pixman-1') libcap = dependency('libcap') libinput = dependency('libinput') +libdbus = dependency('dbus-1', required: false) math = cc.find_library('m') rt = cc.find_library('rt') git = find_program('git', required: false) @@ -46,6 +49,10 @@ if gdk_pixbuf.found() conf_data.set('HAVE_GDK_PIXBUF', true) endif +if enable_tray and libdbus.found() + conf_data.set('ENABLE_TRAY', true) +endif + if a2x.found() mandir = get_option('mandir') man_files = [ diff --git a/meson_options.txt b/meson_options.txt index 541ccf138..cb4f67fde 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,4 @@ option('sway_version', type : 'string', description: 'The version string reported in `sway --version`.') option('default_wallpaper', type: 'boolean', value: true, description: 'Install the default wallpaper.') option('zsh_completions', type: 'boolean', value: true, description: 'Install zsh shell completions.') +option('enable_tray', type: 'boolean', value: true, description: 'Use the swaybar system tray if libDBus is found.') diff --git a/swaybar/bar.c b/swaybar/bar.c index fb4170954..f741315aa 100644 --- a/swaybar/bar.c +++ b/swaybar/bar.c @@ -16,6 +16,11 @@ #else #include #endif +#include "config.h" +#ifdef ENABLE_TRAY +#include "swaybar/tray/dbus.h" +#include "swaybar/tray/tray.h" +#endif #include "swaybar/render.h" #include "swaybar/config.h" #include "swaybar/event_loop.h" @@ -296,6 +301,9 @@ void bar_setup(struct swaybar *bar, } ipc_get_workspaces(bar); render_all_frames(bar); +#ifdef ENABLE_TRAY + init_tray(bar); +#endif } static void display_in(int fd, short mask, void *_bar) { @@ -328,6 +336,9 @@ void bar_run(struct swaybar *bar) { } while (1) { event_loop_poll(); +#ifdef ENABLE_TRAY + dispatch_dbus(); +#endif } } diff --git a/swaybar/meson.build b/swaybar/meson.build index d65edb11e..df1c59239 100644 --- a/swaybar/meson.build +++ b/swaybar/meson.build @@ -1,20 +1,32 @@ -executable( - 'swaybar', [ - 'bar.c', - 'config.c', - 'event_loop.c', - 'i3bar.c', - 'ipc.c', - 'main.c', - 'render.c', - 'status_line.c', - ], +sources = [ + 'bar.c', + 'config.c', + 'event_loop.c', + 'i3bar.c', + 'ipc.c', + 'main.c', + 'render.c', + 'status_line.c' +] + +if enable_tray and libdbus.found() + sources += [ + 'tray/dbus.c', + 'tray/icon.c', + 'tray/sni.c', + 'tray/sni_watcher.c', + 'tray/tray.c' + ] +endif + +executable('swaybar', sources, include_directories: [sway_inc], dependencies: [ cairo, client_protos, gdk_pixbuf, jsonc, + libdbus, math, pango, pangocairo, diff --git a/swaybar/tray/dbus.c b/swaybar/tray/dbus.c new file mode 100644 index 000000000..77b791db2 --- /dev/null +++ b/swaybar/tray/dbus.c @@ -0,0 +1,305 @@ +#define _XOPEN_SOURCE 700 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "swaybar/tray/dbus.h" +#include "swaybar/event_loop.h" +#include "log.h" + +DBusConnection *conn = NULL; + +static void dispatch_watch(int fd, short mask, void *data) { + wlr_log(L_DEBUG, "Dispatching watch"); + DBusWatch *watch = data; + + if (!dbus_watch_get_enabled(watch)) { + return; + } + + uint32_t flags = 0; + + if (mask & POLLIN) { + flags |= DBUS_WATCH_READABLE; + } if (mask & POLLOUT) { + flags |= DBUS_WATCH_WRITABLE; + } if (mask & POLLHUP) { + flags |= DBUS_WATCH_HANGUP; + } if (mask & POLLERR) { + flags |= DBUS_WATCH_ERROR; + } + + dbus_watch_handle(watch, flags); +} + +static dbus_bool_t add_watch(DBusWatch *watch, void *_data) { + if (!dbus_watch_get_enabled(watch)) { + // Watch should not be polled + return TRUE; + } + + short mask = 0; + uint32_t flags = dbus_watch_get_flags(watch); + + if (flags & DBUS_WATCH_READABLE) { + mask |= POLLIN; + } if (flags & DBUS_WATCH_WRITABLE) { + mask |= POLLOUT; + } + + int fd = dbus_watch_get_unix_fd(watch); + + wlr_log(L_DEBUG, "Adding DBus watch fd: %d", fd); + add_event(fd, mask, dispatch_watch, watch); + + return TRUE; +} + +static void remove_watch(DBusWatch *watch, void *_data) { + int fd = dbus_watch_get_unix_fd(watch); + + remove_event(fd); +} + +static void dispatch_timeout(timer_t timer, void *data) { + wlr_log(L_DEBUG, "Dispatching DBus timeout"); + DBusTimeout *timeout = data; + + if (dbus_timeout_get_enabled(timeout)) { + dbus_timeout_handle(timeout); + } +} + +static dbus_bool_t add_timeout(DBusTimeout *timeout, void *_data) { + if (!dbus_timeout_get_enabled(timeout)) { + return TRUE; + } + + timer_t *timer = malloc(sizeof(timer_t)); + if (!timer) { + wlr_log(L_ERROR, "Cannot allocate memory"); + return FALSE; + } + struct sigevent ev = { + .sigev_notify = SIGEV_NONE, + }; + + if (timer_create(CLOCK_MONOTONIC, &ev, timer)) { + wlr_log(L_ERROR, "Could not create DBus timer"); + return FALSE; + } + + int interval = dbus_timeout_get_interval(timeout); + int interval_sec = interval / 1000; + int interval_msec = (interval_sec * 1000) - interval; + + struct timespec period = { + (time_t) interval_sec, + ((long) interval_msec) * 1000 * 1000, + }; + struct itimerspec time = { + period, + period, + }; + + timer_settime(*timer, 0, &time, NULL); + + dbus_timeout_set_data(timeout, timer, NULL); + + wlr_log(L_DEBUG, "Adding DBus timeout. Interval: %ds %dms", + interval_sec, interval_msec); + add_timer(*timer, dispatch_timeout, timeout); + + return TRUE; +} +static void remove_timeout(DBusTimeout *timeout, void *_data) { + timer_t *timer = (timer_t *) dbus_timeout_get_data(timeout); + wlr_log(L_DEBUG, "Removing DBus timeout."); + + if (timer) { + remove_timer(*timer); + timer_delete(*timer); + free(timer); + } +} + +static bool should_dispatch = true; + +static void dispatch_status(DBusConnection *connection, + DBusDispatchStatus new_status, void *_data) { + if (new_status == DBUS_DISPATCH_DATA_REMAINS) { + should_dispatch = true; + } +} + +struct async_prop_data { + char const *sig; + void (*callback)(DBusMessageIter *, void *, enum property_status); + void *usr_data; +}; + +static void get_prop_callback(DBusPendingCall *pending, void *_data) { + struct async_prop_data *data = _data; + + DBusMessage *reply = dbus_pending_call_steal_reply(pending); + + if (!reply) { + wlr_log(L_INFO, "Could not get a property responce"); + goto bail; + } + + if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) { + char *msg; + + dbus_message_get_args(reply, NULL, + DBUS_TYPE_STRING, &msg, + DBUS_TYPE_INVALID); + + wlr_log(L_INFO, "Failure to get property: %s", msg); + data->callback(NULL, data->usr_data, PROP_ERROR); + goto bail; + } + + DBusMessageIter iter; + DBusMessageIter variant; + + dbus_message_iter_init(reply, &iter); + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) { + wlr_log(L_ERROR, "Property relpy type incorrect"); + data->callback(NULL, data->usr_data, PROP_BAD_DATA); + goto bail; + } + dbus_message_iter_recurse(&iter, &variant); + + if (!dbus_message_iter_check_signature(&variant, data->sig)) { + wlr_log(L_INFO, "Property returned has incorrect signatue."); + data->callback(&variant, data->usr_data, PROP_WRONG_SIG); + goto bail; + } + + data->callback(&variant, data->usr_data, PROP_EXISTS); + +bail: + if (reply) { + dbus_message_unref(reply); + } + dbus_pending_call_unref(pending); +} + +/* Public functions below -- see header for docs*/ + +bool dbus_message_iter_check_signature(DBusMessageIter *iter, const char *sig) { + char *msg_sig = dbus_message_iter_get_signature(iter); + int result = strcmp(msg_sig, sig); + dbus_free(msg_sig); + return (result == 0); +} + +bool dbus_get_prop_async(const char *destination, + const char *path, const char *iface, + const char *prop, const char *expected_signature, + void (*callback)(DBusMessageIter *, void *, enum property_status), + void *usr_data) { + struct async_prop_data *data = malloc(sizeof(struct async_prop_data)); + if (!data) { + return false; + } + DBusPendingCall *pending; + DBusMessage *message = dbus_message_new_method_call( + destination, path, + "org.freedesktop.DBus.Properties", + "Get"); + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &prop, + DBUS_TYPE_INVALID); + + bool status = + dbus_connection_send_with_reply(conn, message, &pending, -1); + + dbus_message_unref(message); + + if (!(pending || status)) { + wlr_log(L_ERROR, "Could not get property (%s) from path " + "(%s) on iface (%s)",prop, path, iface ); + return false; + } + + data->sig = expected_signature; + data->callback = callback; + data->usr_data = usr_data; + dbus_pending_call_set_notify(pending, get_prop_callback, data, free); + + return true; +} + +void dispatch_dbus() { + if (!should_dispatch || !conn) { + return; + } + + DBusDispatchStatus status; + + do { + status = dbus_connection_dispatch(conn); + } while (status == DBUS_DISPATCH_DATA_REMAINS); + + if (status != DBUS_DISPATCH_COMPLETE) { + wlr_log(L_ERROR, "Cannot dispatch dbus events: %d", status); + } + + should_dispatch = false; +} + +int dbus_init() { + DBusError error; + dbus_error_init(&error); + + conn = dbus_bus_get(DBUS_BUS_SESSION, &error); + if (conn == NULL) { + wlr_log(L_INFO, "Compiled with dbus support, " \ + "but unable to connect to dbus."); + wlr_log(L_INFO, "swaybar will be unable to display tray icons."); + return -1; + } + + dbus_connection_set_exit_on_disconnect(conn, FALSE); + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "Cannot get DBus connection: %s\n", + error.message); + conn = NULL; + return -1; + } + + wlr_log(L_INFO, "Unique name: %s\n", dbus_bus_get_unique_name(conn)); + + // Will be called if dispatch status changes + dbus_connection_set_dispatch_status_function(conn, dispatch_status, + NULL, NULL); + + if (!dbus_connection_set_watch_functions(conn, add_watch, remove_watch, + NULL, NULL, NULL)) { + dbus_connection_set_watch_functions(conn, + NULL, NULL, NULL, NULL, NULL); + wlr_log(L_ERROR, "Failed to activate DBUS watch functions"); + return -1; + } + + if (!dbus_connection_set_timeout_functions(conn, add_timeout, + remove_timeout, NULL, NULL, NULL)) { + dbus_connection_set_watch_functions(conn, + NULL, NULL, NULL, NULL, NULL); + dbus_connection_set_timeout_functions(conn, + NULL, NULL, NULL, NULL, NULL); + wlr_log(L_ERROR, "Failed to activate DBUS timeout functions"); + return -1; + } + + return 0; +} diff --git a/swaybar/tray/icon.c b/swaybar/tray/icon.c new file mode 100644 index 000000000..447b90294 --- /dev/null +++ b/swaybar/tray/icon.c @@ -0,0 +1,452 @@ +#define _XOPEN_SOURCE 700 +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "swaybar/tray/icon.h" +#include "swaybar/bar.h" +#include "swaybar/config.h" +#include "stringop.h" +#include "log.h" + +/* Finds all themes that the given theme inherits */ +static list_t *find_inherits(const char *theme_dir) { + const char inherits[] = "Inherits"; + const char index_name[] = "/index.theme"; + list_t *themes = create_list(); + FILE *index = NULL; + char *path = malloc(strlen(theme_dir) + sizeof(index_name)); + if (!path) { + goto fail; + } + if (!themes) { + goto fail; + } + + strcpy(path, theme_dir); + strcat(path, index_name); + + index = fopen(path, "r"); + if (!index) { + goto fail; + } + + char *buf = NULL; + size_t n = 0; + while (!feof(index) && getline(&buf, &n, index) != -1) { + if (n <= sizeof(inherits) + 1) { + continue; + } + if (strncmp(inherits, buf, sizeof(inherits) - 1) == 0) { + char *themestr = buf + sizeof(inherits); + themes = split_string(themestr, ","); + break; + } + } + free(buf); + +fail: + free(path); + if (index) { + fclose(index); + } + return themes; +} + +static bool isdir(const char *path) { + struct stat statbuf; + if (stat(path, &statbuf) != -1) { + if (S_ISDIR(statbuf.st_mode)) { + return true; + } + } + return false; + +} + +static bool isfile(const char *path) { + struct stat statbuf; + if (stat(path, &statbuf) != -1) { + if (S_ISREG(statbuf.st_mode) || S_ISLNK(statbuf.st_mode)) { + return true; + } + } + return false; + +} + +/** + * Returns the directory of a given theme if it exists. + * The returned pointer must be freed. + */ +static char *find_theme_dir(const char *theme) { + char *basedir; + char *icon_dir; + + if (!theme) { + return NULL; + } + + if (!(icon_dir = malloc(1024))) { + wlr_log(L_ERROR, "Out of memory!"); + goto fail; + } + + if ((basedir = getenv("HOME"))) { + if (snprintf(icon_dir, 1024, "%s/.icons/%s", basedir, theme) >= 1024) { + wlr_log(L_ERROR, "Path too long to render"); + // XXX perhaps just goto trying in /usr/share? This + // shouldn't happen anyway, but might with a long global + goto fail; + } + + if (isdir(icon_dir)) { + return icon_dir; + } + } + + if ((basedir = getenv("XDG_DATA_DIRS"))) { + if (!(basedir = strdup(basedir))) { + wlr_log_errno(L_ERROR, "Path too long to render"); + goto fail; + } + char *token = strtok(basedir, ":"); + while (token) { + // By peeking at the spec, there should be a slash at + // the end of the data dir. + if (snprintf(icon_dir, 1024, "%sicons/%s", token, theme) >= 1024) { + wlr_log(L_ERROR, "Path too long to render"); + // ditto + free(basedir); + goto fail; + } + + if (isdir(icon_dir)) { + free(basedir); + return icon_dir; + } + token = strtok(NULL, ":"); + } + free(basedir); + } + + // Spec says use "/usr/share/pixmaps/", but I see everything in + // "/usr/share/icons/" look it both, I suppose. + if (snprintf(icon_dir, 1024, "/usr/share/pixmaps/%s", theme) >= 1024) { + wlr_log(L_ERROR, "Path too long to render"); + goto fail; + } + if (isdir(icon_dir)) { + return icon_dir; + } + + if (snprintf(icon_dir, 1024, "/usr/share/icons/%s", theme) >= 1024) { + wlr_log(L_ERROR, "Path too long to render"); + goto fail; + } + if (isdir(icon_dir)) { + return icon_dir; + } + +fail: + free(icon_dir); + wlr_log(L_ERROR, "Could not find dir for theme: %s", theme); + return NULL; +} + +/** + * Returns all theme dirs needed to be looked in for an icon. + * Does not check for duplicates + */ +static list_t *find_all_theme_dirs(const char *theme) { + list_t *dirs = create_list(); + if (!dirs) { + return NULL; + } + char *dir = find_theme_dir(theme); + if (dir) { + list_add(dirs, dir); + list_t *inherits = find_inherits(dir); + list_cat(dirs, inherits); + list_free(inherits); + } + // 'default' usually inherits the default theme. I don't believe it has + // any icons, but look for them anyway + dir = find_theme_dir("default"); + if (dir) { + list_add(dirs, dir); + list_t *inherits = find_inherits(dir); + list_cat(dirs, inherits); + list_free(inherits); + } + dir = find_theme_dir("hicolor"); + if (dir) { + list_add(dirs, dir); + } + + return dirs; +} + +struct subdir { + int size; + char name[]; +}; + +static int subdir_str_cmp(const void *_subdir, const void *_str) { + const struct subdir *subdir = _subdir; + const char *str = _str; + return strcmp(subdir->name, str); +} +/** + * Helper to find_subdirs. Acts similar to `split_string(subdirs, ",")` but + * generates a list of struct subdirs + */ +static list_t *split_subdirs(char *subdir_str) { + list_t *subdir_list = create_list(); + char *copy = strdup(subdir_str); + if (!subdir_list || !copy) { + list_free(subdir_list); + free(copy); + return NULL; + } + + char *token; + token = strtok(copy, ","); + while(token) { + int len = strlen(token) + 1; + struct subdir *subdir = + malloc(sizeof(struct subdir) + sizeof(char [len])); + if (!subdir) { + // Return what we have + return subdir_list; + } + subdir->size = 0; + strcpy(subdir->name, token); + + list_add(subdir_list, subdir); + + token = strtok(NULL, ","); + } + free(copy); + + return subdir_list; +} +/** + * Returns a list of all subdirectories of a theme. + * Take note: the subdir names are all relative to `theme_dir` and must be + * combined with it to form a valid directory. + * + * Each member of the list is of type (struct subdir *) this struct contains + * the name of the subdir, along with size information. These must be freed + * bye the caller. + * + * This currently ignores min and max sizes of icons. + */ +static list_t* find_theme_subdirs(const char *theme_dir) { + const char index_name[] = "/index.theme"; + list_t *dirs = NULL; + char *path = malloc(strlen(theme_dir) + sizeof(index_name)); + FILE *index = NULL; + if (!path) { + wlr_log(L_ERROR, "Failed to allocate memory"); + goto fail; + } + + strcpy(path, theme_dir); + strcat(path, index_name); + + index = fopen(path, "r"); + if (!index) { + wlr_log(L_ERROR, "Could not open file: %s", path); + goto fail; + } + + char *buf = NULL; + size_t n = 0; + const char directories[] = "Directories"; + while (!feof(index) && getline(&buf, &n, index) != -1) { + if (n <= sizeof(directories) + 1) { + continue; + } + if (strncmp(directories, buf, sizeof(directories) - 1) == 0) { + char *dirstr = buf + sizeof(directories); + int len = strlen(dirstr); + if (dirstr[len-1] == '\n') { + dirstr[len-1] = '\0'; + } + dirs = split_subdirs(dirstr); + break; + } + } + // Now, find the size of each dir + struct subdir *current_subdir = NULL; + const char size[] = "Size"; + while (!feof(index) && getline(&buf, &n, index) != -1) { + if (buf[0] == '[') { + int len = strlen(buf); + if (buf[len-1] == '\n') { + len--; + } + // replace ']' + buf[len-1] = '\0'; + + int index; + if ((index = list_seq_find(dirs, subdir_str_cmp, buf+1)) != -1) { + current_subdir = (dirs->items[index]); + } + } + + if (strncmp(size, buf, sizeof(size) - 1) == 0) { + if (current_subdir) { + current_subdir->size = atoi(buf + sizeof(size)); + } + } + } + free(buf); +fail: + free(path); + if (index) { + fclose(index); + } + return dirs; +} + +/* Returns true if full path and file exists */ +static bool is_valid_path(const char *file) { + if (strstr(file, "/") == NULL || !isfile(file)) { + return false; + } +#ifdef WITH_GDK_PIXBUF + if (strstr(file, ".png") == NULL && + strstr(file, ".xpm") == NULL && + strstr(file, ".svg") == NULL) { +#else + if (strstr(file, ".png") == NULL) { +#endif + return false; + } + + return true; +} + +/* Returns the file of an icon given its name and size */ +static char *find_icon_file(const char *name, int size) { + int namelen = strlen(name); + // TODO TRAY icon theme + // list_t *dirs = find_all_theme_dirs(swaybar.config->icon_theme); + list_t *dirs = find_all_theme_dirs(NULL); + if (!dirs) { + return NULL; + } + int min_size_diff = INT_MAX; + char *current_file = NULL; + + for (int i = 0; i < dirs->length; ++i) { + char *dir = dirs->items[i]; + list_t *subdirs = find_theme_subdirs(dir); + + if (!subdirs) { + continue; + } + + for (int i = 0; i < subdirs->length; ++i) { + struct subdir *subdir = subdirs->items[i]; + + // Only use an unsized if we don't already have a + // canidate this should probably change to allow svgs + if (!subdir->size && current_file) { + continue; + } + + int size_diff = abs(size - subdir->size); + + if (size_diff >= min_size_diff) { + continue; + } + + char *path = malloc(strlen(subdir->name) + strlen(dir) + 2); + + strcpy(path, dir); + path[strlen(dir)] = '/'; + strcpy(path + strlen(dir) + 1, subdir->name); + + DIR *icons = opendir(path); + if (!icons) { + free(path); + continue; + } + + struct dirent *direntry; + while ((direntry = readdir(icons)) != NULL) { + int len = strlen(direntry->d_name); + if (len <= namelen + 2) { //must have some ext + continue; + } + if (strncmp(direntry->d_name, name, namelen) == 0) { + char *ext = direntry->d_name + namelen + 1; +#ifdef WITH_GDK_PIXBUF + if (strcmp(ext, "png") == 0 || + strcmp(ext, "xpm") == 0 || + strcmp(ext, "svg") == 0) { +#else + if (strcmp(ext, "png") == 0) { +#endif + free(current_file); + char *icon_path = malloc(strlen(path) + len + 2); + + strcpy(icon_path, path); + icon_path[strlen(path)] = '/'; + strcpy(icon_path + strlen(path) + 1, direntry->d_name); + current_file = icon_path; + min_size_diff = size_diff; + } + } + } + free(path); + closedir(icons); + } + free_flat_list(subdirs); + } + free_flat_list(dirs); + + return current_file; +} + +cairo_surface_t *find_icon(const char *name, int size) { + char *image_path; + if (is_valid_path(name)) { + image_path = strdup(name); + } else { + image_path = find_icon_file(name, size); + } + if (image_path == NULL) { + return NULL; + } + + cairo_surface_t *image = NULL; +#ifdef WITH_GDK_PIXBUF + GError *err = NULL; + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file(image_path, &err); + if (!pixbuf) { + wlr_log(L_ERROR, "Failed to load icon image: %s", err->message); + } + image = gdk_cairo_image_surface_create_from_pixbuf(pixbuf); + g_object_unref(pixbuf); +#else + // TODO make svg work? cairo supports it. maybe remove gdk alltogether + image = cairo_image_surface_create_from_png(image_path); +#endif //WITH_GDK_PIXBUF + if (!image) { + wlr_log(L_ERROR, "Could not read icon image"); + return NULL; + } + + free(image_path); + return image; +} diff --git a/swaybar/tray/sni.c b/swaybar/tray/sni.c new file mode 100644 index 000000000..a8254be7d --- /dev/null +++ b/swaybar/tray/sni.c @@ -0,0 +1,342 @@ +#define _XOPEN_SOURCE 500 +#include +#include +#include +#include +#include +#include +#include +#include +#include "swaybar/tray/dbus.h" +#include "swaybar/tray/sni.h" +#include "swaybar/tray/icon.h" +#include "swaybar/bar.h" +#include "cairo.h" +#include "log.h" + +static const char *KDE_IFACE = "org.kde.StatusNotifierItem"; +static const char *FD_IFACE = "org.freedesktop.StatusNotifierItem"; + +// Not sure what this is but cairo needs it. +static const cairo_user_data_key_t cairo_user_data_key; + +struct sni_icon_ref *sni_icon_ref_create(struct StatusNotifierItem *item, + int height) { + struct sni_icon_ref *sni_ref = malloc(sizeof(struct sni_icon_ref)); + if (!sni_ref) { + return NULL; + } + sni_ref->icon = cairo_image_surface_scale(item->image, height, height); + sni_ref->ref = item; + + return sni_ref; +} + +void sni_icon_ref_free(struct sni_icon_ref *sni_ref) { + if (!sni_ref) { + return; + } + cairo_surface_destroy(sni_ref->icon); + free(sni_ref); +} + +/* Gets the pixmap of an icon */ +static void reply_icon(DBusMessageIter *iter /* a(iiay) */, void *_data, + enum property_status status) { + if (status != PROP_EXISTS) { + return; + } + struct StatusNotifierItem *item = _data; + + DBusMessageIter d_struct; /* (iiay) */ + DBusMessageIter struct_items; + DBusMessageIter icon; + + if (dbus_message_iter_get_element_count(iter) == 0) { + // Can't recurse if there are no items + wlr_log(L_INFO, "Item has no icon"); + return; + } + + dbus_message_iter_recurse(iter, &d_struct); + dbus_message_iter_recurse(&d_struct, &struct_items); + + int width; + dbus_message_iter_get_basic(&struct_items, &width); + dbus_message_iter_next(&struct_items); + + int height; + dbus_message_iter_get_basic(&struct_items, &height); + dbus_message_iter_next(&struct_items); + + int len = dbus_message_iter_get_element_count(&struct_items); + + if (!len) { + wlr_log(L_ERROR, "No icon data"); + return; + } + + // Also implies len % 4 == 0, useful below + if (len != width * height * 4) { + wlr_log(L_ERROR, "Incorrect array size passed"); + return; + } + + dbus_message_iter_recurse(&struct_items, &icon); + + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + // FIXME support a variable stride + // (works on my machine though for all tested widths) + if (!sway_assert(stride == width * 4, "Stride must be equal to byte length")) { + return; + } + + // Data is by reference, no need to free + uint8_t *message_data; + dbus_message_iter_get_fixed_array(&icon, &message_data, &len); + + uint8_t *image_data = malloc(stride * height); + if (!image_data) { + wlr_log(L_ERROR, "Could not allocate memory for icon"); + return; + } + + // Transform from network byte order to host byte order + // Assumptions are safe because the equality above + uint32_t *network = (uint32_t *) message_data; + uint32_t *host = (uint32_t *)image_data; + for (int i = 0; i < width * height; ++i) { + host[i] = ntohl(network[i]); + } + + cairo_surface_t *image = cairo_image_surface_create_for_data( + image_data, CAIRO_FORMAT_ARGB32, + width, height, stride); + + if (image) { + if (item->image) { + cairo_surface_destroy(item->image); + } + item->image = image; + // Free the image data on surface destruction + cairo_surface_set_user_data(image, + &cairo_user_data_key, + image_data, + free); + item->dirty = true; + // TODO TRAY rerender + //dirty = true; + + return; + } else { + wlr_log(L_ERROR, "Could not create image surface"); + free(image_data); + } + + wlr_log(L_ERROR, "Could not get icon from item"); + return; +} + +/* Get an icon by its name */ +static void reply_icon_name(DBusMessageIter *iter, void *_data, enum property_status status) { + struct StatusNotifierItem *item = _data; + + if (status != PROP_EXISTS) { + dbus_get_prop_async(item->name, item->object_path, + (item->kde_special_snowflake ? KDE_IFACE : FD_IFACE), + "IconPixmap", "a(iiay)", reply_icon, item); + return; + } + + char *icon_name; + dbus_message_iter_get_basic(iter, &icon_name); + + cairo_surface_t *image = find_icon(icon_name, 256); + + if (image) { + wlr_log(L_DEBUG, "Icon for %s found with size %d", icon_name, + cairo_image_surface_get_width(image)); + if (item->image) { + cairo_surface_destroy(item->image); + } + item->image = image; + item->dirty = true; + // TODO TRAY rerender + //dirty = true; + + return; + } + + // Now try the pixmap + dbus_get_prop_async(item->name, item->object_path, + (item->kde_special_snowflake ? KDE_IFACE : FD_IFACE), + "IconPixmap", "a(iiay)", reply_icon, item); +} + +void get_icon(struct StatusNotifierItem *item) { + dbus_get_prop_async(item->name, item->object_path, + (item->kde_special_snowflake ? KDE_IFACE : FD_IFACE), + "IconName", "s", reply_icon_name, item); +} + +void sni_activate(struct StatusNotifierItem *item, uint32_t x, uint32_t y) { + const char *iface = + (item->kde_special_snowflake ? "org.kde.StatusNotifierItem" + : "org.freedesktop.StatusNotifierItem"); + DBusMessage *message = dbus_message_new_method_call( + item->name, + item->object_path, + iface, + "Activate"); + + dbus_message_append_args(message, + DBUS_TYPE_INT32, &x, + DBUS_TYPE_INT32, &y, + DBUS_TYPE_INVALID); + + dbus_connection_send(conn, message, NULL); + + dbus_message_unref(message); +} + +void sni_context_menu(struct StatusNotifierItem *item, uint32_t x, uint32_t y) { + const char *iface = + (item->kde_special_snowflake ? "org.kde.StatusNotifierItem" + : "org.freedesktop.StatusNotifierItem"); + wlr_log(L_INFO, "Activating context menu for item: (%s,%s)", item->name, item->object_path); + DBusMessage *message = dbus_message_new_method_call( + item->name, + item->object_path, + iface, + "ContextMenu"); + + dbus_message_append_args(message, + DBUS_TYPE_INT32, &x, + DBUS_TYPE_INT32, &y, + DBUS_TYPE_INVALID); + + dbus_connection_send(conn, message, NULL); + + dbus_message_unref(message); +} +void sni_secondary(struct StatusNotifierItem *item, uint32_t x, uint32_t y) { + const char *iface = + (item->kde_special_snowflake ? "org.kde.StatusNotifierItem" + : "org.freedesktop.StatusNotifierItem"); + DBusMessage *message = dbus_message_new_method_call( + item->name, + item->object_path, + iface, + "SecondaryActivate"); + + dbus_message_append_args(message, + DBUS_TYPE_INT32, &x, + DBUS_TYPE_INT32, &y, + DBUS_TYPE_INVALID); + + dbus_connection_send(conn, message, NULL); + + dbus_message_unref(message); +} + +static void get_unique_name(struct StatusNotifierItem *item) { + // I think that we're fine being sync here becaues the message is + // directly to the message bus. Could be async though. + DBusMessage *message = dbus_message_new_method_call( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "GetNameOwner"); + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &item->name, + DBUS_TYPE_INVALID); + + DBusMessage *reply = dbus_connection_send_with_reply_and_block( + conn, message, -1, NULL); + + dbus_message_unref(message); + + if (!reply) { + wlr_log(L_ERROR, "Could not get unique name for item: %s", + item->name); + return; + } + + char *unique_name; + if (!dbus_message_get_args(reply, NULL, + DBUS_TYPE_STRING, &unique_name, + DBUS_TYPE_INVALID)) { + wlr_log(L_ERROR, "Error parsing method args"); + } else { + if (item->unique_name) { + free(item->unique_name); + } + item->unique_name = strdup(unique_name); + } + + dbus_message_unref(reply); +} + +struct StatusNotifierItem *sni_create(const char *name) { + // Make sure `name` is well formed + if (!dbus_validate_bus_name(name, NULL)) { + wlr_log(L_INFO, "Name (%s) is not a bus name. We cannot create an item.", name); + return NULL; + } + + struct StatusNotifierItem *item = malloc(sizeof(struct StatusNotifierItem)); + item->name = strdup(name); + item->unique_name = NULL; + // TODO use static str if the default path instead of all these god-damn strdups + item->object_path = strdup("/StatusNotifierItem"); + item->image = NULL; + item->dirty = false; + + // If it doesn't use this name then assume that it uses the KDE spec + // This is because xembed-sni-proxy uses neither "org.freedesktop" nor + // "org.kde" and just gives us the items "unique name" + // + // We could use this to our advantage and fill out the "unique name" + // field with the given name if it is neither freedesktop or kde, but + // that's makes us rely on KDE hackyness which is bad practice + const char freedesktop_name[] = "org.freedesktop"; + if (strncmp(name, freedesktop_name, sizeof(freedesktop_name) - 1) != 0) { + item->kde_special_snowflake = true; + } else { + item->kde_special_snowflake = false; + } + + get_icon(item); + + get_unique_name(item); + + return item; +} +struct StatusNotifierItem *sni_create_from_obj_path(const char *unique_name, + const char *object_path) { + struct StatusNotifierItem *item = malloc(sizeof(struct StatusNotifierItem)); + // XXX strdup-ing twice to avoid a double-free; see above todo + item->name = strdup(unique_name); + item->unique_name = strdup(unique_name); + item->object_path = strdup(object_path); + item->image = NULL; + item->dirty = false; + // If they're registering by obj-path they're a special snowflake + item->kde_special_snowflake = true; + + get_icon(item); + return item; +} +void sni_free(struct StatusNotifierItem *item) { + if (!item) { + return; + } + free(item->name); + free(item->unique_name); + free(item->object_path); + if (item->image) { + cairo_surface_destroy(item->image); + } + free(item); +} diff --git a/swaybar/tray/sni_watcher.c b/swaybar/tray/sni_watcher.c new file mode 100644 index 000000000..4ab2f0c1c --- /dev/null +++ b/swaybar/tray/sni_watcher.c @@ -0,0 +1,638 @@ +#define _XOPEN_SOURCE 500 +#include +#include +#include +#include +#include +#include +#include "swaybar/tray/dbus.h" +#include "list.h" +#include "log.h" + +static list_t *items = NULL; +static list_t *hosts = NULL; +static list_t *object_path_items = NULL; + +/** + * Describes the function of the StatusNotifierWatcher + * See https://freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierWatcher/ + * + * We also implement KDE's special snowflake protocol, it's like this but with + * all occurrences 'freedesktop' replaced with 'kde'. There is no KDE introspect. + * + * FIXME Treat object path items like KDE does + */ +static const char *interface_xml = + "" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + +struct ObjPathItem { + char *obj_path; + char *unique_name; +}; + +static void free_obj_path_item(struct ObjPathItem *item) { + if (!item) { + return; + } + free(item->unique_name); + free(item->obj_path); + free(item); +} +static struct ObjPathItem *create_obj_path_item(const char *unique_name, + const char *obj_path) { + struct ObjPathItem *item = malloc(sizeof(struct ObjPathItem)); + if (!item) { + return NULL; + } + item->unique_name = strdup(unique_name); + item->obj_path = strdup(obj_path); + if (!item->unique_name || !item->obj_path) { + free_obj_path_item(item); + return NULL; + } + return item; +} +/** + * NOTE: This compare function does have ordering, this is because it has to + * comapre two strings. + */ +static int obj_path_item_cmp(const void *_item1, const void *_item2) { + const struct ObjPathItem *item1 = _item1; + const struct ObjPathItem *item2 = _item2; + if (strcmp(item1->unique_name,item2->unique_name) == 0 && + strcmp(item1->obj_path,item2->obj_path) == 0) { + return 0; + } + return -1; +} +static int obj_path_unique_name_cmp(const void *_item, + const void *_unique_name) { + const struct ObjPathItem *item = _item; + const char *unique_name = _unique_name; + return strcmp(item->unique_name, unique_name); +} + +static void host_registered_signal(DBusConnection *connection) { + // Send one signal for each protocol + DBusMessage *signal = dbus_message_new_signal( + "/StatusNotifierWatcher", + "org.freedesktop.StatusNotifierWatcher", + "StatusNotifierHostRegistered"); + + dbus_connection_send(connection, signal, NULL); + dbus_message_unref(signal); + + + signal = dbus_message_new_signal( + "/StatusNotifierWatcher", + "org.kde.StatusNotifierWatcher", + "StatusNotifierHostRegistered"); + + dbus_connection_send(connection, signal, NULL); + dbus_message_unref(signal); +} + +static void item_registered_signal(DBusConnection *connection, const char *name) { + DBusMessage *signal = dbus_message_new_signal( + "/StatusNotifierWatcher", + "org.freedesktop.StatusNotifierWatcher", + "StatusNotifierItemRegistered"); + dbus_message_append_args(signal, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID); + dbus_connection_send(connection, signal, NULL); + dbus_message_unref(signal); + + signal = dbus_message_new_signal( + "/StatusNotifierWatcher", + "org.kde.StatusNotifierWatcher", + "StatusNotifierItemRegistered"); + dbus_message_append_args(signal, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID); + dbus_connection_send(connection, signal, NULL); + dbus_message_unref(signal); +} + +static void item_unregistered_signal(DBusConnection *connection, const char *name) { + DBusMessage *signal = dbus_message_new_signal( + "/StatusNotifierWatcher", + "org.freedesktop.StatusNotifierWatcher", + "StatusNotifierItemUnregistered"); + dbus_message_append_args(signal, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID); + dbus_connection_send(connection, signal, NULL); + dbus_message_unref(signal); + + signal = dbus_message_new_signal( + "/StatusNotifierWatcher", + "org.kde.StatusNotifierWatcher", + "StatusNotifierItemUnregistered"); + dbus_message_append_args(signal, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID); + dbus_connection_send(connection, signal, NULL); + dbus_message_unref(signal); +} + +static void obj_path_item_registered_signal(DBusConnection *connection, + const struct ObjPathItem *item) { + DBusMessage *signal = dbus_message_new_signal( + "/StatusNotifierWatcher", + "org.swaywm.LessSuckyStatusNotifierWatcher", + "ObjPathItemRegistered"); + dbus_message_append_args(signal, + DBUS_TYPE_OBJECT_PATH, &item->obj_path, + DBUS_TYPE_STRING, &item->unique_name, + DBUS_TYPE_INVALID); + dbus_connection_send(connection, signal, NULL); + dbus_message_unref(signal); +} + +static void respond_to_introspect(DBusConnection *connection, DBusMessage *request) { + DBusMessage *reply; + + reply = dbus_message_new_method_return(request); + dbus_message_append_args(reply, + DBUS_TYPE_STRING, &interface_xml, + DBUS_TYPE_INVALID); + dbus_connection_send(connection, reply, NULL); + dbus_message_unref(reply); +} + +static void register_item(DBusConnection *connection, DBusMessage *message) { + DBusError error; + DBusMessage *reply; + char *name; + + dbus_error_init(&error); + if (!dbus_message_get_args(message, &error, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) { + wlr_log(L_ERROR, "Error parsing method args: %s", error.message); + } + + wlr_log(L_INFO, "RegisterStatusNotifierItem called with \"%s\"", name); + + // Don't add duplicate or not real item + if (!dbus_validate_bus_name(name, NULL)) { + if (dbus_validate_path(name, NULL)) { + // Item is registered by object path + struct ObjPathItem *item = + create_obj_path_item(dbus_message_get_sender(message), name); + + // Add ObjPathItem + if (list_seq_find(object_path_items, obj_path_item_cmp, item) != -1) { + free_obj_path_item(item); + return; + } + list_add(object_path_items, item); + obj_path_item_registered_signal(connection, item); + goto send_reply; + } else { + wlr_log(L_INFO, "This item is not valid, we cannot keep track of it."); + return; + } + } else { + + if (list_seq_find(items, (int (*)(const void *, const void *))strcmp, name) != -1) { + return; + } + if (!dbus_bus_name_has_owner(connection, name, &error)) { + return; + } + + list_add(items, strdup(name)); + item_registered_signal(connection, name); + } + +send_reply: + // It's silly, but clients want a reply for this function + reply = dbus_message_new_method_return(message); + dbus_connection_send(connection, reply, NULL); + dbus_message_unref(reply); +} + +static void register_host(DBusConnection *connection, DBusMessage *message) { + DBusError error; + char *name; + + dbus_error_init(&error); + if (!dbus_message_get_args(message, &error, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) { + wlr_log(L_ERROR, "Error parsing method args: %s", error.message); + } + + wlr_log(L_INFO, "RegisterStatusNotifierHost called with \"%s\"", name); + + // Don't add duplicate or not real host + if (!dbus_validate_bus_name(name, NULL)) { + wlr_log(L_INFO, "This item is not valid, we cannot keep track of it."); + return; + } + + + if (list_seq_find(hosts, (int (*)(const void *, const void *))strcmp, name) != -1) { + return; + } + if (!dbus_bus_name_has_owner(connection, name, &error)) { + return; + } + + list_add(hosts, strdup(name)); + host_registered_signal(connection); +} + +static void get_property(DBusConnection *connection, DBusMessage *message) { + DBusError error; + char *interface; + char *property; + + dbus_error_init(&error); + if (!dbus_message_get_args(message, &error, + DBUS_TYPE_STRING, &interface, + DBUS_TYPE_STRING, &property, + DBUS_TYPE_INVALID)) { + wlr_log(L_ERROR, "Error parsing prop args: %s", error.message); + return; + } + + if (strcmp(property, "RegisteredStatusNotifierItems") == 0) { + wlr_log(L_INFO, "Replying with items"); + DBusMessage *reply; + reply = dbus_message_new_method_return(message); + DBusMessageIter iter; + DBusMessageIter sub; + DBusMessageIter subsub; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_VARIANT, + "as", &sub); + dbus_message_iter_open_container(&sub, DBUS_TYPE_ARRAY, + "s", &subsub); + + for (int i = 0; i < items->length; ++i) { + dbus_message_iter_append_basic(&subsub, + DBUS_TYPE_STRING, &items->items[i]); + } + + dbus_message_iter_close_container(&sub, &subsub); + dbus_message_iter_close_container(&iter, &sub); + + dbus_connection_send(connection, reply, NULL); + dbus_message_unref(reply); + } else if (strcmp(property, "IsStatusNotifierHostRegistered") == 0) { + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter sub; + int registered = (hosts == NULL || hosts->length == 0) ? 0 : 1; + + reply = dbus_message_new_method_return(message); + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_VARIANT, + "b", &sub); + dbus_message_iter_append_basic(&sub, + DBUS_TYPE_BOOLEAN, ®istered); + + dbus_message_iter_close_container(&iter, &sub); + + dbus_connection_send(connection, reply, NULL); + dbus_message_unref(reply); + } else if (strcmp(property, "ProtocolVersion") == 0) { + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter sub; + const int version = 0; + + reply = dbus_message_new_method_return(message); + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_VARIANT, + "i", &sub); + dbus_message_iter_append_basic(&sub, + DBUS_TYPE_INT32, &version); + + dbus_message_iter_close_container(&iter, &sub); + dbus_connection_send(connection, reply, NULL); + dbus_message_unref(reply); + } else if (strcmp(property, "RegisteredObjectPathItems") == 0) { + wlr_log(L_INFO, "Replying with ObjPathItems"); + DBusMessage *reply; + reply = dbus_message_new_method_return(message); + DBusMessageIter iter; + DBusMessageIter variant; + DBusMessageIter array; + DBusMessageIter dstruct; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_VARIANT, + "a(os)", &variant); + dbus_message_iter_open_container(&variant, DBUS_TYPE_ARRAY, + "(os)", &array); + + for (int i = 0; i < object_path_items->length; ++i) { + struct ObjPathItem *item = object_path_items->items[i]; + + dbus_message_iter_open_container(&array, + DBUS_TYPE_STRUCT, NULL, &dstruct); + + dbus_message_iter_append_basic(&dstruct, + DBUS_TYPE_OBJECT_PATH, &item->obj_path); + dbus_message_iter_append_basic(&dstruct, + DBUS_TYPE_STRING, &item->unique_name); + + dbus_message_iter_close_container(&array, &dstruct); + } + + dbus_message_iter_close_container(&variant, &array); + dbus_message_iter_close_container(&iter, &variant); + + dbus_connection_send(connection, reply, NULL); + dbus_message_unref(reply); + } +} + +static void set_property(DBusConnection *connection, DBusMessage *message) { + // All properties are read only and we don't allow new properties + return; +} + +// TODO clean me up please or get rid of me +// also add LessSuckyStatusNotifierWatcher props +static void get_all(DBusConnection *connection, DBusMessage *message) { + DBusMessage *reply; + reply = dbus_message_new_method_return(message); + DBusMessageIter iter; /* a{v} */ + DBusMessageIter arr; + DBusMessageIter dict; + DBusMessageIter sub; + DBusMessageIter subsub; + int registered = (hosts == NULL || hosts->length == 0) ? 0 : 1; + const int version = 0; + const char *prop; + + // Could clean this up with a function for each prop + dbus_message_iter_init_append(reply, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + "{sv}", &arr); + + prop = "RegisteredStatusNotifierItems"; + dbus_message_iter_open_container(&arr, DBUS_TYPE_DICT_ENTRY, + NULL, &dict); + dbus_message_iter_append_basic(&dict, + DBUS_TYPE_STRING, &prop); + dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, + "as", &sub); + dbus_message_iter_open_container(&sub, DBUS_TYPE_ARRAY, + "s", &subsub); + for (int i = 0; i < items->length; ++i) { + dbus_message_iter_append_basic(&subsub, + DBUS_TYPE_STRING, &items->items[i]); + } + dbus_message_iter_close_container(&sub, &subsub); + dbus_message_iter_close_container(&dict, &sub); + dbus_message_iter_close_container(&arr, &dict); + + prop = "IsStatusNotifierHostRegistered"; + dbus_message_iter_open_container(&arr, DBUS_TYPE_DICT_ENTRY, + NULL, &dict); + dbus_message_iter_append_basic(&dict, + DBUS_TYPE_STRING, &prop); + dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, + "b", &sub); + dbus_message_iter_append_basic(&sub, + DBUS_TYPE_BOOLEAN, ®istered); + dbus_message_iter_close_container(&dict, &sub); + dbus_message_iter_close_container(&arr, &dict); + + prop = "ProtocolVersion"; + dbus_message_iter_open_container(&arr, DBUS_TYPE_DICT_ENTRY, + NULL, &dict); + dbus_message_iter_append_basic(&dict, + DBUS_TYPE_STRING, &prop); + dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, + "i", &sub); + dbus_message_iter_append_basic(&sub, + DBUS_TYPE_INT32, &version); + dbus_message_iter_close_container(&dict, &sub); + dbus_message_iter_close_container(&arr, &dict); + + dbus_message_iter_close_container(&iter, &arr); + + dbus_connection_send(connection, reply, NULL); + dbus_message_unref(reply); +} + +static DBusHandlerResult message_handler(DBusConnection *connection, + DBusMessage *message, void *data) { + const char *interface_name = dbus_message_get_interface(message); + const char *member_name = dbus_message_get_member(message); + + // In order of the xml above + if (strcmp(interface_name, "org.freedesktop.DBus.Introspectable") == 0 && + strcmp(member_name, "Introspect") == 0) { + // We don't have an introspect for KDE + respond_to_introspect(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } else if (strcmp(interface_name, "org.freedesktop.DBus.Properties") == 0) { + if (strcmp(member_name, "Get") == 0) { + get_property(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } else if (strcmp(member_name, "Set") == 0) { + set_property(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } else if (strcmp(member_name, "GetAll") == 0) { + get_all(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } else { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + } else if (strcmp(interface_name, "org.freedesktop.StatusNotifierWatcher") == 0 || + strcmp(interface_name, "org.kde.StatusNotifierWatcher") == 0) { + if (strcmp(member_name, "RegisterStatusNotifierItem") == 0) { + register_item(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } else if (strcmp(member_name, "RegisterStatusNotifierHost") == 0) { + register_host(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } else { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + } + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static DBusHandlerResult signal_handler(DBusConnection *connection, + DBusMessage *message, void *_data) { + if (dbus_message_is_signal(message, "org.freedesktop.DBus", "NameOwnerChanged")) { + // Only eat the message if it is name that we are watching + const char *name; + const char *old_owner; + const char *new_owner; + int index; + bool found_obj_path_item = false; + + if (!dbus_message_get_args(message, NULL, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &old_owner, + DBUS_TYPE_STRING, &new_owner, + DBUS_TYPE_INVALID)) { + wlr_log(L_ERROR, "Error getting LostName args"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + if (strcmp(new_owner, "") != 0) { + // Name is not lost + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + if ((index = list_seq_find(items, (int (*)(const void *, const void *))strcmp, name)) != -1) { + wlr_log(L_INFO, "Status Notifier Item lost %s", name); + free(items->items[index]); + list_del(items, index); + item_unregistered_signal(connection, name); + + return DBUS_HANDLER_RESULT_HANDLED; + } + if ((index = list_seq_find(hosts, (int (*)(const void *, const void *))strcmp, name)) != -1) { + wlr_log(L_INFO, "Status Notifier Host lost %s", name); + free(hosts->items[index]); + list_del(hosts, index); + + return DBUS_HANDLER_RESULT_HANDLED; + } + while ((index = list_seq_find(object_path_items, obj_path_unique_name_cmp, name)) != -1) { + found_obj_path_item = true; + struct ObjPathItem *item = object_path_items->items[index]; + wlr_log(L_INFO, "ObjPathItem lost %s", item->obj_path); + list_del(object_path_items, index); + free_obj_path_item(item); + } + if (found_obj_path_item) { + item_unregistered_signal(connection, name); + return DBUS_HANDLER_RESULT_HANDLED; + } + } + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static const DBusObjectPathVTable vtable = { + .message_function = message_handler, + .unregister_function = NULL, +}; + +int init_sni_watcher() { + DBusError error; + dbus_error_init(&error); + if (!conn) { + wlr_log(L_ERROR, "Connection is null, cannot initiate StatusNotifierWatcher"); + return -1; + } + + items = create_list(); + hosts = create_list(); + object_path_items = create_list(); + + int status = dbus_bus_request_name(conn, "org.freedesktop.StatusNotifierWatcher", + DBUS_NAME_FLAG_REPLACE_EXISTING, + &error); + if (status == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + wlr_log(L_DEBUG, "Got watcher name"); + } else if (status == DBUS_REQUEST_NAME_REPLY_IN_QUEUE) { + wlr_log(L_INFO, "Could not get watcher name, it may start later"); + } + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "dbus err getting watcher name: %s", error.message); + return -1; + } + + status = dbus_bus_request_name(conn, "org.kde.StatusNotifierWatcher", + DBUS_NAME_FLAG_REPLACE_EXISTING, + &error); + if (status == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + wlr_log(L_DEBUG, "Got kde watcher name"); + } else if (status == DBUS_REQUEST_NAME_REPLY_IN_QUEUE) { + wlr_log(L_INFO, "Could not get kde watcher name, it may start later"); + } + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "dbus err getting kde watcher name: %s", error.message); + return -1; + } + + dbus_connection_try_register_object_path(conn, + "/StatusNotifierWatcher", + &vtable, NULL, &error); + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "dbus_err: %s", error.message); + return -1; + } + + dbus_bus_add_match(conn, + "type='signal',\ + sender='org.freedesktop.DBus',\ + interface='org.freedesktop.DBus',\ + member='NameOwnerChanged'", + &error); + + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "DBus error getting match args: %s", error.message); + } + + dbus_connection_add_filter(conn, signal_handler, NULL, NULL); + return 0; +} diff --git a/swaybar/tray/tray.c b/swaybar/tray/tray.c new file mode 100644 index 000000000..6d9568dcd --- /dev/null +++ b/swaybar/tray/tray.c @@ -0,0 +1,516 @@ +#define _XOPEN_SOURCE 700 +#include +#include +#include +#include +#include +#include +#include "cairo.h" +#include "swaybar/bar.h" +#include "swaybar/tray/tray.h" +#include "swaybar/tray/dbus.h" +#include "swaybar/tray/sni.h" +#include "swaybar/tray/sni_watcher.h" +#include "swaybar/bar.h" +#include "swaybar/config.h" +#include "list.h" +#include "log.h" + +// TODO TRAY remove globals +static struct tray tray; + +static void register_host(char *name) { + DBusMessage *message; + + message = dbus_message_new_method_call( + "org.freedesktop.StatusNotifierWatcher", + "/StatusNotifierWatcher", + "org.freedesktop.StatusNotifierWatcher", + "RegisterStatusNotifierHost"); + if (!message) { + wlr_log(L_ERROR, "Cannot allocate dbus method call"); + return; + } + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID); + + dbus_connection_send(conn, message, NULL); + + dbus_message_unref(message); +} + +static void get_items_reply(DBusMessageIter *iter, void *_data, + enum property_status status) { + if (status != PROP_EXISTS) { + return; + } + DBusMessageIter array; + + // O(n) function, could be faster dynamically reading values + int len = dbus_message_iter_get_element_count(iter); + + dbus_message_iter_recurse(iter, &array); + for (int i = 0; i < len; i++) { + const char *name; + dbus_message_iter_get_basic(&array, &name); + + bool found = false;; + struct StatusNotifierItem *item; + wl_list_for_each(item, &tray.items, link) { + if (strcmp(item->name, name) == 0) { + found = true; + break; + } + } + + if (!found) { + struct StatusNotifierItem *item = sni_create(name); + + if (item) { + wlr_log(L_DEBUG, "Item registered with host: %s", + name); + wl_list_insert(&tray.items, &item->link); + // TODO TRAY render + //dirty = true; + } + } + } +} +static void get_obj_items_reply(DBusMessageIter *iter, void *_data, + enum property_status status) { + if (status != PROP_EXISTS) { + return; + } + DBusMessageIter array; + DBusMessageIter dstruct; + + int len = dbus_message_iter_get_element_count(iter); + + dbus_message_iter_recurse(iter, &array); + for (int i = 0; i < len; i++) { + const char *object_path; + const char *unique_name; + + dbus_message_iter_recurse(&array, &dstruct); + + dbus_message_iter_get_basic(&dstruct, &object_path); + dbus_message_iter_next(&dstruct); + dbus_message_iter_get_basic(&dstruct, &unique_name); + + bool found = false;; + struct StatusNotifierItem *item; + wl_list_for_each(item, &tray.items, link) { + if (strcmp(item->name, object_path) == 0 && + strcmp(item->unique_name, unique_name) == 0) { + found = true; + } + } + + if (!found) { + item = sni_create_from_obj_path(unique_name, object_path); + + if (item) { + wlr_log(L_DEBUG, "Item registered with host: %s", + unique_name); + wl_list_insert(tray.items.prev, &item->link); + // TODO TRAY + //dirty = true; + } + } + } +} + +static void get_items() { + // Clear list + struct StatusNotifierItem *item, *tmp; + wl_list_for_each_safe(item, tmp, &tray.items, link) { + wl_list_remove(&item->link); + sni_free(item); + } + + dbus_get_prop_async("org.freedesktop.StatusNotifierWatcher", + "/StatusNotifierWatcher","org.freedesktop.StatusNotifierWatcher", + "RegisteredStatusNotifierItems", "as", get_items_reply, NULL); + + dbus_get_prop_async("org.freedesktop.StatusNotifierWatcher", + "/StatusNotifierWatcher","org.swaywm.LessSuckyStatusNotifierWatcher", + "RegisteredObjectPathItems", "a(os)", get_obj_items_reply, NULL); +} + +static DBusHandlerResult signal_handler(DBusConnection *connection, + DBusMessage *message, void *_data) { + if (dbus_message_is_signal(message, "org.freedesktop.StatusNotifierWatcher", + "StatusNotifierItemRegistered")) { + const char *name; + if (!dbus_message_get_args(message, NULL, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) { + wlr_log(L_ERROR, "Error getting StatusNotifierItemRegistered args"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + bool found = false;; + struct StatusNotifierItem *item; + wl_list_for_each(item, &tray.items, link) { + if (strcmp(item->name, name) == 0) { + found = true; + break; + } + } + if (!found) { + struct StatusNotifierItem *item = sni_create(name); + + if (item) { + wl_list_insert(&tray.items, &item->link); + // TODO TRAY render + //dirty = true; + } + } + + return DBUS_HANDLER_RESULT_HANDLED; + } else if (dbus_message_is_signal(message, "org.freedesktop.StatusNotifierWatcher", + "StatusNotifierItemUnregistered")) { + const char *name; + if (!dbus_message_get_args(message, NULL, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) { + wlr_log(L_ERROR, "Error getting StatusNotifierItemUnregistered args"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + bool found = false;; + struct StatusNotifierItem *item, *tmp; + wl_list_for_each_safe(item, tmp, &tray.items, link) { + if (strcmp(item->name, name) == 0) { + found = true; + wl_list_remove(&item->link); + sni_free(item); + // TODO TRAY render + //dirty = true; + } + } + if (!found) { + // If it's not in our list, then our list is incorrect. + // Fetch all items again + wlr_log(L_INFO, "Host item list incorrect, refreshing"); + get_items(); + } + + return DBUS_HANDLER_RESULT_HANDLED; + } else if (dbus_message_is_signal(message, "org.freedesktop.StatusNotifierItem", + "NewIcon") || dbus_message_is_signal(message, + "org.kde.StatusNotifierItem", "NewIcon")) { + const char *name; + const char *obj_path; + struct StatusNotifierItem *item; + + name = dbus_message_get_sender(message); + obj_path = dbus_message_get_path(message); + + wl_list_for_each(item, &tray.items, link) { + if (strcmp(item->name, name) == 0 && + strcmp(item->unique_name, obj_path) == 0) { + get_icon(item); + } + } + + return DBUS_HANDLER_RESULT_HANDLED; + } else if (dbus_message_is_signal(message, + "org.swaywm.LessSuckyStatusNotifierWatcher", + "ObjPathItemRegistered")) { + const char *object_path; + const char *unique_name; + if (!dbus_message_get_args(message, NULL, + DBUS_TYPE_OBJECT_PATH, &object_path, + DBUS_TYPE_STRING, &unique_name, + DBUS_TYPE_INVALID)) { + wlr_log(L_ERROR, "Error getting ObjPathItemRegistered args"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + bool found = false;; + struct StatusNotifierItem *item; + wl_list_for_each(item, &tray.items, link) { + if (strcmp(item->name, object_path) == 0 && + strcmp(item->unique_name, unique_name) == 0) { + found = true; + } + } + if (!found) { + item = sni_create_from_obj_path(unique_name, object_path); + + if (item) { + wl_list_insert(&tray.items, &item->link); + // TODO TRAY render + //dirty = true; + } + } + + return DBUS_HANDLER_RESULT_HANDLED; + } + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static int init_host() { + wl_list_init(&tray.items); + + DBusError error; + dbus_error_init(&error); + char *name = NULL; + if (!conn) { + wlr_log(L_ERROR, "Connection is null, cannot init SNI host"); + goto err; + } + name = calloc(sizeof(char), 256); + + if (!name) { + wlr_log(L_ERROR, "Cannot allocate name"); + goto err; + } + + pid_t pid = getpid(); + if (snprintf(name, 256, "org.freedesktop.StatusNotifierHost-%d", pid) + >= 256) { + wlr_log(L_ERROR, "Cannot get host name because string is too short." + "This should not happen"); + goto err; + } + + // We want to be the sole owner of this name + if (dbus_bus_request_name(conn, name, DBUS_NAME_FLAG_DO_NOT_QUEUE, + &error) != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + wlr_log(L_ERROR, "Cannot get host name and start the tray"); + goto err; + } + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "Dbus err getting host name: %s\n", error.message); + goto err; + } + wlr_log(L_DEBUG, "Got host name"); + + register_host(name); + + // Chances are if an item is already running, we'll get it two times. + // Once from this and another time from queued signals. Still we want + // to do this to be a complient sni host just in case. + get_items(); + + // Perhaps use addmatch helper functions like wlc does? + dbus_bus_add_match(conn, + "type='signal',\ + sender='org.freedesktop.StatusNotifierWatcher',\ + member='StatusNotifierItemRegistered'", + &error); + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "dbus_err: %s", error.message); + goto err; + } + dbus_bus_add_match(conn, + "type='signal',\ + sender='org.freedesktop.StatusNotifierWatcher',\ + member='StatusNotifierItemUnregistered'", + &error); + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "dbus_err: %s", error.message); + return -1; + } + dbus_bus_add_match(conn, + "type='signal',\ + sender='org.freedesktop.StatusNotifierWatcher',\ + member='ObjPathItemRegistered'", + &error); + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "dbus_err: %s", error.message); + return -1; + } + + // SNI matches + dbus_bus_add_match(conn, + "type='signal',\ + interface='org.freedesktop.StatusNotifierItem',\ + member='NewIcon'", + &error); + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "dbus_err %s", error.message); + goto err; + } + dbus_bus_add_match(conn, + "type='signal',\ + interface='org.kde.StatusNotifierItem',\ + member='NewIcon'", + &error); + if (dbus_error_is_set(&error)) { + wlr_log(L_ERROR, "dbus_err %s", error.message); + goto err; + } + + dbus_connection_add_filter(conn, signal_handler, NULL, NULL); + + free(name); + return 0; + +err: + // TODO better handle errors + free(name); + return -1; +} + +// TODO tray mouse +//void tray_mouse_event(struct output *output, int rel_x, int rel_y, +// uint32_t button, uint32_t state) { +// +// int x = rel_x; +// int y = rel_y + (swaybar.config->position == DESKTOP_SHELL_PANEL_POSITION_TOP +// ? 0 : (output->state->height - output->window->height)); +// +// struct window *window = output->window; +// uint32_t tray_padding = swaybar.config->tray_padding; +// int tray_width = window->width * window->scale; +// +// for (int i = 0; i < output->items->length; ++i) { +// struct sni_icon_ref *item = +// output->items->items[i]; +// int icon_width = cairo_image_surface_get_width(item->icon); +// +// tray_width -= tray_padding; +// if (x <= tray_width && x >= tray_width - icon_width) { +// if (button == swaybar.config->activate_button) { +// sni_activate(item->ref, x, y); +// } else if (button == swaybar.config->context_button) { +// sni_context_menu(item->ref, x, y); +// } else if (button == swaybar.config->secondary_button) { +// sni_secondary(item->ref, x, y); +// } +// break; +// } +// tray_width -= icon_width; +// } +//} + +uint32_t render_tray(cairo_t *cairo, struct swaybar_output *output, + struct swaybar_config *config, struct swaybar_workspace *ws, + double *pos, uint32_t height) { + + double original_width = *pos; + + // Tray icons + double tray_padding = (double) config->tray_padding; + // TODO TRAY scaling + const double item_size = ((double) height) - (2. * tray_padding); + + if (item_size < 0) { + // Can't render items if the padding is too large + return height; + } + + // TODO TRAY outputs + //if (config->tray_output && strcmp(config->tray_output, output->name) != 0) { + // return tray_width; + //} + + //bool clean_item = false; + //// Clean item if only one output has tray or this is the last output + //if (swaybar.outputs->length == 1 || config->tray_output || + // output->idx == swaybar.outputs->length-1) { + // clean_item = true; + //// More trickery is needed in case you plug off secondary outputs on live + //} else { + // int active_outputs = 0; + // for (int i = 0; i < swaybar.outputs->length; i++) { + // struct output *output = swaybar.outputs->items[i]; + // if (output->active) { + // active_outputs++; + // } + // } + // if (active_outputs == 1) { + // clean_item = true; + // } + //} + + struct wl_list *curr_node = output->item_refs.next; + + struct StatusNotifierItem *item; + wl_list_for_each(item, &tray.items, link) { + if (!item->image) { + continue; + } + + struct sni_icon_ref *render_item = NULL; + + while (curr_node != &output->item_refs) { + struct sni_icon_ref *ref = + wl_container_of(curr_node, render_item, link); + struct wl_list *next = curr_node->next; + + if (ref->ref == item) { + render_item = ref; + break; + } else { + wl_list_remove(curr_node); + sni_icon_ref_free(ref); + } + + curr_node = next; + } + + if (!render_item) { + // We ran out of item refs without finding the current + // item so we need to add it. + render_item = sni_icon_ref_create(item, item_size); + + wl_list_insert(curr_node->prev, &render_item->link); + } else if (item->dirty) { + // item needs re-render + wlr_log(L_DEBUG, "Redrawing item %s for output %s", + item->name, output->name); + struct sni_icon_ref *new = + sni_icon_ref_create(item, item_size); + + // Replace the current node with the new one + struct wl_list *tmp = curr_node->prev; + wl_list_remove(curr_node); + sni_icon_ref_free(render_item); + wl_list_insert(tmp, &new->link); + render_item = new; + } + + *pos -= tray_padding; + *pos -= item_size; + + // Now draw it + cairo_operator_t op = cairo_get_operator(cairo); + cairo_set_operator(cairo, CAIRO_OPERATOR_OVER); + cairo_set_source_surface(cairo, render_item->icon, *pos, + tray_padding); + cairo_rectangle(cairo, *pos, tray_padding, item_size, + item_size); + cairo_fill(cairo); + cairo_set_operator(cairo, op); + + item->dirty = false; + } + + // If we drew any items, add another padding + // TODO TRAY scale + if (original_width != *pos) { + *pos -= tray_padding; + } + + return height; +} + +void init_tray(struct swaybar *bar) { + tray.bar = bar; + + // TODO TRAY outputs only init tray if an output exists. + /* Connect to the D-Bus */ + dbus_init(); + + /* Start the SNI watcher */ + init_sni_watcher(); + + /* Start the SNI host */ + init_host(); +}