From eda814755afd8c078c543c486927c45a8ada7d6f Mon Sep 17 00:00:00 2001
From: Orestis Floros <orestisflo@gmail.com>
Date: Fri, 11 Oct 2019 17:04:10 +0300
Subject: [PATCH] Make tray icon order deterministic

Fixes #3573
---
 i3bar/include/trayclients.h |   3 +
 i3bar/src/xcb.c             | 118 +++++++++++++++++++++++++++++++-----
 2 files changed, 107 insertions(+), 14 deletions(-)

diff --git a/i3bar/include/trayclients.h b/i3bar/include/trayclients.h
index db954bb1..3f215ce4 100644
--- a/i3bar/include/trayclients.h
+++ b/i3bar/include/trayclients.h
@@ -18,6 +18,9 @@ struct trayclient {
     bool mapped;      /* Whether this window is mapped */
     int xe_version;   /* The XEMBED version supported by the client */
 
+    char *class_class;
+    char *class_instance;
+
     TAILQ_ENTRY(trayclient)
     tailq; /* Pointer for the TAILQ-Macro */
 };
diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c
index 30ce06b7..26ff3ded 100644
--- a/i3bar/src/xcb.c
+++ b/i3bar/src/xcb.c
@@ -692,6 +692,31 @@ static void handle_visibility_notify(xcb_visibility_notify_event_t *event) {
     }
 }
 
+static int strcasecmp_nullable(const char *a, const char *b) {
+    if (a == b) {
+        return 0;
+    }
+    if (a == NULL) {
+        return -1;
+    }
+    if (b == NULL) {
+        return 1;
+    }
+    return strcasecmp(a, b);
+}
+
+/*
+ * Sort trayclients in descending order
+ *
+ */
+static int reorder_trayclients_cmp(const void *_a, const void *_b) {
+    trayclient *a = *((trayclient **)_a);
+    trayclient *b = *((trayclient **)_b);
+
+    int result = strcasecmp_nullable(a->class_class, b->class_class);
+    return result != 0 ? result : strcasecmp_nullable(a->class_instance, b->class_instance);
+}
+
 /*
  * Adjusts the size of the tray window and alignment of the tray clients by
  * configuring their respective x coordinates. To be called when mapping or
@@ -699,26 +724,42 @@ static void handle_visibility_notify(xcb_visibility_notify_event_t *event) {
  *
  */
 static void configure_trayclients(void) {
-    trayclient *trayclient;
     i3_output *output;
     SLIST_FOREACH(output, outputs, slist) {
-        if (!output->active)
+        if (!output->active) {
             continue;
+        }
 
-        int clients = 0;
-        TAILQ_FOREACH_REVERSE(trayclient, output->trayclients, tc_head, tailq) {
-            if (!trayclient->mapped)
-                continue;
-            clients++;
+        int count = 0;
+        trayclient *client;
+        TAILQ_FOREACH(client, output->trayclients, tailq) {
+            if (client->mapped) {
+                count++;
+            }
+        }
 
-            DLOG("Configuring tray window %08x to x=%d\n",
-                 trayclient->win, output->rect.w - (clients * (icon_size + logical_px(config.tray_padding))));
-            uint32_t x = output->rect.w - (clients * (icon_size + logical_px(config.tray_padding)));
+        int idx = 0;
+        trayclient **trayclients = smalloc(count * sizeof(trayclient *));
+        TAILQ_FOREACH(client, output->trayclients, tailq) {
+            if (client->mapped) {
+                trayclients[idx++] = client;
+            }
+        }
+
+        qsort(trayclients, count, sizeof(trayclient *), reorder_trayclients_cmp);
+
+        uint32_t x = output->rect.w;
+        for (idx = count; idx > 0; idx--) {
+            x -= icon_size + logical_px(config.tray_padding);
+
+            DLOG("Configuring tray window %08x to x=%d\n", trayclients[idx - 1]->win, x);
             xcb_configure_window(xcb_connection,
-                                 trayclient->win,
+                                 trayclients[idx - 1]->win,
                                  XCB_CONFIG_WINDOW_X,
                                  &x);
         }
+
+        free(trayclients);
     }
 }
 
@@ -746,6 +787,45 @@ static trayclient *trayclient_from_window(xcb_window_t win) {
     return trayclient_and_output_from_window(win, NULL);
 }
 
+static void trayclient_update_class(trayclient *client) {
+    xcb_get_property_reply_t *prop = xcb_get_property_reply(
+        conn,
+        xcb_get_property_unchecked(
+            xcb_connection,
+            false,
+            client->win,
+            XCB_ATOM_WM_CLASS,
+            XCB_ATOM_STRING,
+            0,
+            32),
+        NULL);
+    if (prop == NULL || xcb_get_property_value_length(prop) == 0) {
+        DLOG("WM_CLASS not set.\n");
+        free(prop);
+        return;
+    }
+
+    /* We cannot use asprintf here since this property contains two
+     * null-terminated strings (for compatibility reasons). Instead, we
+     * use strdup() on both strings */
+    const size_t prop_length = xcb_get_property_value_length(prop);
+    char *new_class = xcb_get_property_value(prop);
+    const size_t class_class_index = strnlen(new_class, prop_length) + 1;
+
+    free(client->class_instance);
+    free(client->class_class);
+
+    client->class_instance = sstrndup(new_class, prop_length);
+    if (class_class_index < prop_length) {
+        client->class_class = sstrndup(new_class + class_class_index, prop_length - class_class_index);
+    } else {
+        client->class_class = NULL;
+    }
+    DLOG("WM_CLASS changed to %s (instance), %s (class)\n", client->class_instance, client->class_class);
+
+    free(prop);
+}
+
 /*
  * Handles ClientMessages (messages sent from another client directly to us).
  *
@@ -876,11 +956,12 @@ static void handle_client_message(xcb_client_message_event_t *event) {
              * exits/crashes. */
             xcb_change_save_set(xcb_connection, XCB_SET_MODE_INSERT, client);
 
-            trayclient *tc = smalloc(sizeof(trayclient));
+            trayclient *tc = scalloc(1, sizeof(trayclient));
             tc->win = client;
             tc->xe_version = xe_version;
             tc->mapped = false;
             TAILQ_INSERT_TAIL(output_for_tray->trayclients, tc, tailq);
+            trayclient_update_class(tc);
 
             if (map_it) {
                 DLOG("Mapping dock client\n");
@@ -968,15 +1049,16 @@ static void handle_unmap_notify(xcb_unmap_notify_event_t *event) {
 }
 
 /*
- * Handle PropertyNotify messages. Currently only the _XEMBED_INFO property is
- * handled, which tells us whether a dock client should be mapped or unmapped.
+ * Handle PropertyNotify messages.
  *
  */
 static void handle_property_notify(xcb_property_notify_event_t *event) {
     DLOG("PropertyNotify\n");
     if (event->atom == atoms[_XEMBED_INFO] &&
         event->state == XCB_PROPERTY_NEW_VALUE) {
+        /* _XEMBED_INFO property tells us whether a dock client should be mapped or unmapped. */
         DLOG("xembed_info updated\n");
+
         trayclient *client = trayclient_from_window(event->window);
         if (!client) {
             ELOG("PropertyNotify received for unknown window %08x\n", event->window);
@@ -1014,6 +1096,11 @@ static void handle_property_notify(xcb_property_notify_event_t *event) {
             xcb_map_window(xcb_connection, client->win);
         }
         free(xembedr);
+    } else if (event->atom == XCB_ATOM_WM_CLASS) {
+        trayclient *client = trayclient_from_window(event->window);
+        if (client) {
+            trayclient_update_class(client);
+        }
     }
 }
 
@@ -1532,6 +1619,9 @@ void kick_tray_clients(i3_output *output) {
                             0,
                             0);
 
+        free(trayclient->class_class);
+        free(trayclient->class_instance);
+
         /* We remove the trayclient right here. We might receive an UnmapNotify
          * event afterwards, but better safe than sorry. */
         TAILQ_REMOVE(output->trayclients, trayclient, tailq);