aboutsummaryrefslogtreecommitdiff
path: root/systray/menu.c
diff options
context:
space:
mode:
Diffstat (limited to 'systray/menu.c')
-rw-r--r--systray/menu.c757
1 files changed, 757 insertions, 0 deletions
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);
+}