From 843ad38b3c427adb0bf319e9613d9813c8d9246c Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Wed, 7 Jun 2017 16:45:28 -0700 Subject: Implement Tray Icons This commit implements the StatusNotifierItem protocol, and enables swaybar to show tray icons. It also uses `xembedsniproxy` in order to communicate with xembed applications. The tray is completely optional, and can be disabled on compile time with the `enable-tray` option. Or on runtime with the bar config option `tray_output none`. Overview of changes: In swaybar very little is changed outside the tray subfolder except that all events are now polled in `event_loop.c`, this creates no functional difference. Six bar configuration options were added, these are detailed in sway-bar(5) The tray subfolder is where all protocol implementation takes place and is organised as follows: tray/sni_watcher.c: This file contains the StatusNotifierWatcher. It keeps track of items and hosts and reports when they come or go. tray/tray.c This file contains the StatusNotifierHost. It keeps track of sway's version of the items and represents the tray itself. tray/sni.c This file contains the StatusNotifierItem struct and all communication with individual items. tray/icon.c This file implements the icon theme protocol. It allows for finding icons by name, rather than by pixmap. tray/dbus.c This file allows for asynchronous DBus communication. See #986 #343 --- swaybar/tray/sni_watcher.c | 487 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 487 insertions(+) create mode 100644 swaybar/tray/sni_watcher.c (limited to 'swaybar/tray/sni_watcher.c') diff --git a/swaybar/tray/sni_watcher.c b/swaybar/tray/sni_watcher.c new file mode 100644 index 00000000..388e181d --- /dev/null +++ b/swaybar/tray/sni_watcher.c @@ -0,0 +1,487 @@ +#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; + +/** + * 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. + */ +static const char *interface_xml = + "" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + +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 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; + char *name; + + dbus_error_init(&error); + if (!dbus_message_get_args(message, &error, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) { + sway_log(L_ERROR, "Error parsing method args: %s\n", error.message); + } + + name = strdup(name); + sway_log(L_INFO, "RegisterStatusNotifierItem called with \"%s\"\n", name); + + // Don't add duplicate or not real item + 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, name); + item_registered_signal(connection, name); + + // It's silly, but xembedsniproxy wants a reply for this function + DBusMessage *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)) { + sway_log(L_ERROR, "Error parsing method args: %s\n", error.message); + } + + sway_log(L_INFO, "RegisterStatusNotifierHost called with \"%s\"\n", name); + + // Don't add duplicate or not real host + 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)) { + sway_log(L_ERROR, "Error parsing prop args: %s\n", error.message); + return; + } + + if (strcmp(property, "RegisteredStatusNotifierItems") == 0) { + sway_log(L_INFO, "Replying with items\n"); + 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); + } +} + +static void set_property(DBusConnection *connection, DBusMessage *message) { + // All properties are read only and we don't allow new properties + return; +} + +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; + if (!dbus_message_get_args(message, NULL, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &old_owner, + DBUS_TYPE_STRING, &new_owner, + DBUS_TYPE_INVALID)) { + sway_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) { + sway_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) { + sway_log(L_INFO, "Status Notifier Host lost %s", name); + free(hosts->items[index]); + list_del(hosts, index); + + 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) { + sway_log(L_ERROR, "Connection is null, cannot initiate StatusNotifierWatcher"); + return -1; + } + + items = create_list(); + hosts = 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) { + sway_log(L_DEBUG, "Got watcher name"); + } else if (status == DBUS_REQUEST_NAME_REPLY_IN_QUEUE) { + sway_log(L_INFO, "Could not get watcher name, it may start later"); + } + if (dbus_error_is_set(&error)) { + sway_log(L_ERROR, "dbus err getting watcher name: %s\n", 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) { + sway_log(L_DEBUG, "Got kde watcher name"); + } else if (status == DBUS_REQUEST_NAME_REPLY_IN_QUEUE) { + sway_log(L_INFO, "Could not get kde watcher name, it may start later"); + } + if (dbus_error_is_set(&error)) { + sway_log(L_ERROR, "dbus err getting kde watcher name: %s\n", error.message); + return -1; + } + + dbus_connection_try_register_object_path(conn, + "/StatusNotifierWatcher", + &vtable, NULL, &error); + if (dbus_error_is_set(&error)) { + sway_log(L_ERROR, "dbus_err: %s\n", 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)) { + sway_log(L_ERROR, "DBus error getting match args: %s", error.message); + } + + dbus_connection_add_filter(conn, signal_handler, NULL, NULL); + return 0; +} -- cgit v1.2.3-70-g09d2