Description: <short summary of the patch>
 TODO: Put a short summary on the line above and replace this paragraph
 with a longer explanation of this change. Complete the meta-information
 with other relevant fields (see below for details). To make it easier, the
 information below has been extracted from the changelog. Adjust it or drop
 it.
 .
 rofi (1.6.0-1.0antix1) unstable; urgency=medium
 .
   * antiX builds
Author: anticapitalista <antix@operamail.com>

---
The information above should follow the Patch Tagging Guidelines, please
checkout http://dep.debian.net/deps/dep3/ to learn about the format. Here
are templates for supplementary fields that you might want to add:

Origin: <vendor|upstream|other>, <url of original patch>
Bug: <url in upstream bugtracker>
Bug-Debian: https://bugs.debian.org/<bugnumber>
Bug-Ubuntu: https://launchpad.net/bugs/<bugnumber>
Forwarded: <no|not-needed|url proving that it has been forwarded>
Reviewed-By: <name and email of someone who approved the patch>
Last-Update: 2020-11-12

--- /dev/null
+++ rofi-1.6.0/subprojects/libgwater/libgwater.m4
@@ -0,0 +1,78 @@
+AC_DEFUN([GW_INIT], [
+    AC_REQUIRE([PKG_PROG_PKG_CONFIG])
+
+    gw_glib_min_version="2.36"
+
+    m4_define([GW_INIT])
+])
+
+# $1 component
+# $2 canonalized component
+# $3 [user optional] prefix
+# $4 [optional] pkg-config packages
+# $5 [optional] headers
+AC_DEFUN([_GW_CHECK_INTERNAL], [
+    PKG_CHECK_MODULES([$3][_INTERNAL], [glib-2.0 >= ${gw_glib_min_version} $4])
+
+    m4_ifnblank([$5], [
+        [$2]_missing_headers=""
+        AC_CHECK_HEADERS([$5], [], [], [m4_foreach_w([_gw_header], [$5], [
+            [#]ifdef AS_TR_CPP([HAVE_]_gw_header)
+            [#]include <_gw_header>
+            [#]endif
+        ])])
+        m4_foreach_w([_gw_header], [$5], [
+            AS_IF([test x${]AS_TR_SH([ac_cv_header_]_gw_header)[} != xyes], [[$2]_missing_headers="${[$2]_missing_headers} ]_gw_header["])
+        ])
+        AS_IF([test -n "${[$2]_missing_headers}"], [
+            AC_MSG_ERROR([Missing headers for libgwater-[$1]: ${[$2]_missing_headers}])
+        ])
+    ])
+])
+
+# $1 component
+# $2 [optional] pkg-config packages
+# $3 [optional] headers
+# $4 [user optional] prefix
+AC_DEFUN([_GW_CHECK], [
+    AC_REQUIRE([GW_INIT])
+    _GW_CHECK_INTERNAL([$1], AS_TR_SH([gw_][$1]), AS_TR_CPP([GW_][$1]), [$2], [$3])
+])
+
+AC_DEFUN([GW_CHECK_WAYLAND], [
+    gw_wayland_wayland_min_version="1.1.91"
+
+    _GW_CHECK([wayland], [wayland-client >= ${gw_wayland_wayland_min_version} $1], [errno.h $2])
+])
+
+AC_DEFUN([GW_CHECK_WAYLAND_SERVER], [
+    gw_wayland_wayland_min_version="1.1.91"
+
+    _GW_CHECK([wayland-server], [wayland-server >= ${gw_wayland_wayland_min_version} $1], [errno.h $2])
+])
+
+AC_DEFUN([GW_CHECK_XCB], [
+    _GW_CHECK([xcb], [xcb $1], [stdlib.h $2])
+])
+
+AC_DEFUN([GW_CHECK_MPD], [
+    gw_mpd_gio_unix=
+    PKG_CHECK_EXISTS([gio-unix-2.0], [gw_mpd_have_gio_unix=yes], [gw_mpd_have_gio_unix=no])
+    AS_IF([test x${gw_mpd_have_gio_unix} = xyes], [
+        gw_mpd_gio_unix="gio-unix-2.0"
+    ])
+
+    _GW_CHECK([mpd], [libmpdclient gobject-2.0 gio-2.0 ${gw_mpd_gio_unix} $1], [errno.h sys/types.h sys/socket.h $2])
+])
+
+AC_DEFUN([GW_CHECK_ALSA_MIXER], [
+    _GW_CHECK([alsa-mixer], [alsa $1], [sys/poll.h $2])
+])
+
+AC_DEFUN([GW_CHECK_NL], [
+    _GW_CHECK([nl], [libnl-3.0 $1], [linux/netlink.h $2])
+])
+
+AC_DEFUN([GW_CHECK_WIN], [
+    _GW_CHECK([win], [$1], [Windows.h $2])
+])
--- /dev/null
+++ rofi-1.6.0/subprojects/libgwater/xcb/libgwater-xcb.c
@@ -0,0 +1,182 @@
+/*
+ * libgwater-xcb - XCB GSource
+ *
+ * Copyright © 2014-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "GWaterXcb"
+
+#include <stdlib.h>
+
+#include <glib.h>
+
+#include <xcb/xcb.h>
+
+#include "libgwater-xcb.h"
+
+struct _GWaterXcbSource {
+    GSource source;
+    gboolean connection_owned;
+    xcb_connection_t *connection;
+    gpointer fd;
+    GQueue *queue;
+};
+
+static void
+_g_water_xcb_source_event_free(gpointer data)
+{
+    free(data);
+}
+
+static gboolean
+_g_water_xcb_source_prepare(GSource *source, gint *timeout)
+{
+    GWaterXcbSource *self = (GWaterXcbSource *)source;
+    xcb_flush(self->connection);
+    *timeout = -1;
+    return ! g_queue_is_empty(self->queue);
+}
+
+static gboolean
+_g_water_xcb_source_check(GSource *source)
+{
+    GWaterXcbSource *self = (GWaterXcbSource *)source;
+
+    GIOCondition revents;
+    revents = g_source_query_unix_fd(source, self->fd);
+
+    if ( revents & G_IO_IN )
+    {
+        xcb_generic_event_t *event;
+
+        if ( xcb_connection_has_error(self->connection) )
+            return TRUE;
+
+        while ( ( event = xcb_poll_for_event(self->connection) ) != NULL )
+            g_queue_push_tail(self->queue, event);
+    }
+
+    return ! g_queue_is_empty(self->queue);
+}
+
+static gboolean
+_g_water_xcb_source_dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
+{
+    GWaterXcbSource *self = (GWaterXcbSource *)source;
+    xcb_generic_event_t *event;
+
+    gboolean ret;
+
+    event = g_queue_pop_head(self->queue);
+    ret = ((GWaterXcbEventCallback)callback)(event, user_data);
+    _g_water_xcb_source_event_free(event);
+
+    return ret;
+}
+
+static void
+_g_water_xcb_source_finalize(GSource *source)
+{
+    GWaterXcbSource *self = (GWaterXcbSource *)source;
+
+    g_queue_free_full(self->queue, _g_water_xcb_source_event_free);
+
+    if ( self->connection_owned )
+        xcb_disconnect(self->connection);
+}
+
+static GSourceFuncs _g_water_xcb_source_funcs = {
+    .prepare  = _g_water_xcb_source_prepare,
+    .check    = _g_water_xcb_source_check,
+    .dispatch = _g_water_xcb_source_dispatch,
+    .finalize = _g_water_xcb_source_finalize,
+};
+
+GWaterXcbSource *
+g_water_xcb_source_new(GMainContext *context, const gchar *display, gint *screen, GWaterXcbEventCallback callback, gpointer user_data, GDestroyNotify destroy_func)
+{
+    g_return_val_if_fail(callback != NULL, NULL);
+
+    xcb_connection_t *connection;
+    GWaterXcbSource *self;
+
+    connection = xcb_connect(display, screen);
+    if ( xcb_connection_has_error(connection) )
+    {
+        xcb_disconnect(connection);
+        return NULL;
+    }
+
+    self = g_water_xcb_source_new_for_connection(context, connection, callback, user_data, destroy_func);
+    self->connection_owned = TRUE;
+    return self;
+}
+
+GWaterXcbSource *
+g_water_xcb_source_new_for_connection(GMainContext *context, xcb_connection_t *connection, GWaterXcbEventCallback callback, gpointer user_data, GDestroyNotify destroy_func)
+{
+    g_return_val_if_fail(connection != NULL, NULL);
+    g_return_val_if_fail(callback != NULL, NULL);
+
+    GSource *source;
+    GWaterXcbSource *self;
+
+    source = g_source_new(&_g_water_xcb_source_funcs, sizeof(GWaterXcbSource));
+    self = (GWaterXcbSource *)source;
+    self->connection = connection;
+
+    self->queue = g_queue_new();
+
+    self->fd = g_source_add_unix_fd(source, xcb_get_file_descriptor(self->connection), G_IO_IN);
+
+    g_source_attach(source, context);
+
+    g_source_set_callback(source, (GSourceFunc)callback, user_data, destroy_func);
+
+    return self;
+}
+
+void
+g_water_xcb_source_free(GWaterXcbSource *self)
+{
+    GSource * source = (GSource *)self;
+    g_return_if_fail(self != NULL);
+
+    g_source_destroy(source);
+
+    g_source_unref(source);
+}
+
+xcb_connection_t *
+g_water_xcb_source_get_connection(GWaterXcbSource *self)
+{
+    g_return_val_if_fail(self != NULL, NULL);
+
+    return self->connection;
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libgwater/xcb/libgwater-xcb.h
@@ -0,0 +1,43 @@
+/*
+ * libgwater-xcb - XCB GSource
+ *
+ * Copyright © 2014-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef __G_WATER_XCB_H__
+#define __G_WATER_XCB_H__
+
+G_BEGIN_DECLS
+
+typedef struct _GWaterXcbSource GWaterXcbSource;
+
+typedef gboolean (*GWaterXcbEventCallback)(xcb_generic_event_t *event, gpointer user_data);
+
+GWaterXcbSource *g_water_xcb_source_new(GMainContext *context, const gchar *display, gint *screen, GWaterXcbEventCallback callback, gpointer user_data, GDestroyNotify destroy_func);
+GWaterXcbSource *g_water_xcb_source_new_for_connection(GMainContext *context, xcb_connection_t *connection, GWaterXcbEventCallback callback, gpointer user_data, GDestroyNotify destroy_func);
+void g_water_xcb_source_free(GWaterXcbSource *self);
+
+xcb_connection_t *g_water_xcb_source_get_connection(GWaterXcbSource *source);
+
+G_END_DECLS
+
+#endif /* __G_WATER_XCB_H__ */
--- /dev/null
+++ rofi-1.6.0/subprojects/libgwater/xcb/meson.build
@@ -0,0 +1,18 @@
+project('libgwater-xcb', 'c')
+
+glib_min_version='2.36'
+
+glib = dependency('glib-2.0', version: '>= @0@'.format(glib_min_version))
+xcb = dependency('xcb')
+
+libgwater_xcb_inc = include_directories('.')
+libgwater_xcb_dep = [ xcb, glib ]
+libgwater_xcb_lib = static_library('libgwater-xcb', files(
+        'libgwater-xcb.h',
+        'libgwater-xcb.c',
+    ),
+    dependencies: libgwater_xcb_dep,
+    include_directories: libgwater_xcb_inc,
+)
+
+libgwater_xcb = declare_dependency(link_with: libgwater_xcb_lib, include_directories: libgwater_xcb_inc, dependencies: libgwater_xcb_dep)
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/libnkutils-common.mk
@@ -0,0 +1,276 @@
+#
+# libnkutils - Miscellaneous utilities
+#
+# Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+check_PROGRAMS += \
+	$(_libnkutils_tests)
+
+TESTS += \
+	$(_libnkutils_tests)
+
+EXTRA_DIST += \
+	%D%/man/libnkutils-man.xml \
+	%D%/tests/gtk-3.0/settings.ini \
+	%D%/tests/gtk-4.0/settings.ini \
+	%D%/tests/icons/recursive-theme-test/index.theme \
+	%D%/tests/icons/recursive-theme-test/test-dir/test-icon.svg \
+	$(null)
+
+
+NKUTILS_CFLAGS = \
+	-I$(srcdir)/%D%/src \
+	$(_NKUTILS_INTERNAL_UUID_CFLAGS) \
+	$(_NKUTILS_INTERNAL_XKBCOMMON_CFLAGS) \
+	$(_NKUTILS_INTERNAL_GIO_CFLAGS) \
+	$(_NKUTILS_INTERNAL_GOBJECT_CFLAGS) \
+	$(_NKUTILS_INTERNAL_GLIB_CFLAGS)
+
+_NKUTILS_INTERNAL_CFLAGS = \
+	-DSRCDIR=\"$(srcdir)/%D%\" \
+	-DSYSCONFDIR=\"$(sysconfdir)\" \
+	-DDATADIR=\"$(datadir)\" \
+	$(null)
+
+NKUTILS_LIBS = \
+	$(_libnkutils_library) \
+	$(_NKUTILS_INTERNAL_UUID_LIBS) \
+	$(_NKUTILS_INTERNAL_XKBCOMMON_LIBS) \
+	$(_NKUTILS_INTERNAL_GIO_LIBS) \
+	$(_NKUTILS_INTERNAL_GOBJECT_LIBS) \
+	$(_NKUTILS_INTERNAL_GLIB_LIBS)
+
+NKUTILS_XSLTPROCFLAGS = \
+	--path "$(srcdir)/%D%/man/"
+
+NKUTILS_MANFILES = \
+	%D%/man/libnkutils-man.xml
+
+
+_libnkutils_sources =
+_libnkutils_tests =
+
+if NK_ENABLE_UUID_LIBUUID
+_libnkutils_sources += \
+	%D%/src/uuid-libuuid.c \
+	%D%/src/uuid-internal.h \
+	%D%/src/uuid.c \
+	%D%/src/nkutils-uuid.h
+
+_NKUTILS_INTERNAL_UUID_CFLAGS = \
+	$(_NKUTILS_INTERNAL_UUID_LIBUUID_CFLAGS)
+
+_NKUTILS_INTERNAL_UUID_LIBS = \
+	$(_NKUTILS_INTERNAL_UUID_LIBUUID_LIBS)
+
+_libnkutils_tests += \
+	%D%/tests/uuid.test
+else
+if NK_ENABLE_UUID_APR_UTIL
+_libnkutils_sources += \
+	%D%/src/uuid-apr-util.c \
+	%D%/src/uuid-internal.h \
+	%D%/src/uuid.c \
+	%D%/src/nkutils-uuid.h
+
+_NKUTILS_INTERNAL_UUID_CFLAGS = \
+	$(_NKUTILS_INTERNAL_UUID_APR_UTIL_CFLAGS)
+
+_NKUTILS_INTERNAL_UUID_LIBS = \
+	$(_NKUTILS_INTERNAL_UUID_APR_UTIL_LIBS)
+
+_libnkutils_tests += \
+	%D%/tests/uuid.test
+endif
+endif
+
+if NK_ENABLE_ENUM
+_libnkutils_sources += \
+	%D%/src/enum.c \
+	%D%/src/nkutils-enum.h
+
+_libnkutils_tests += \
+	%D%/tests/enum.test
+endif
+
+if NK_ENABLE_TOKEN
+_libnkutils_sources += \
+	%D%/src/token.c \
+	%D%/src/nkutils-token.h
+
+_libnkutils_tests += \
+	%D%/tests/token.test
+endif
+
+if NK_ENABLE_COLOUR
+_libnkutils_sources += \
+	%D%/src/colour.c \
+	%D%/src/nkutils-colour.h
+
+_libnkutils_tests += \
+	%D%/tests/colour.test
+endif
+
+if NK_ENABLE_GTK_SETTINGS
+_libnkutils_sources += \
+	%D%/src/gtk-settings.c \
+	%D%/src/nkutils-gtk-settings.h
+
+_libnkutils_tests += \
+	%D%/tests/gtk-settings.test
+endif
+
+if NK_ENABLE_XDG_DE
+_libnkutils_sources += \
+	%D%/src/xdg-de.c \
+	%D%/src/nkutils-xdg-de.h
+
+_libnkutils_tests += \
+	%D%/tests/xdg-de.test
+endif
+
+if NK_ENABLE_XDG_THEME
+_libnkutils_sources += \
+	%D%/src/xdg-theme.c \
+	%D%/src/nkutils-xdg-theme.h
+
+_libnkutils_tests += \
+	%D%/tests/xdg-theme.test
+endif
+
+if NK_ENABLE_BINDINGS
+_libnkutils_sources += \
+	%D%/src/bindings.c \
+	%D%/src/nkutils-bindings.h
+
+_libnkutils_tests += \
+	%D%/tests/bindings.test
+endif
+
+
+#
+# Tests
+#
+
+# enum
+%C%_tests_enum_test_SOURCES = \
+	%D%/tests/enum.c
+
+%C%_tests_enum_test_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(NKUTILS_CFLAGS) \
+	$(_NKUTILS_INTERNAL_CFLAGS)
+
+%C%_tests_enum_test_LDADD = \
+	$(NKUTILS_LIBS) \
+	$(_NKUTILS_INTERNAL_TEST_LIBS)
+
+# token
+%C%_tests_token_test_SOURCES = \
+	%D%/tests/token.c
+
+%C%_tests_token_test_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(NKUTILS_CFLAGS) \
+	$(_NKUTILS_INTERNAL_CFLAGS)
+
+%C%_tests_token_test_LDADD = \
+	$(NKUTILS_LIBS) \
+	$(_NKUTILS_INTERNAL_TEST_LIBS)
+
+# colour
+%C%_tests_colour_test_SOURCES = \
+	%D%/tests/colour.c
+
+%C%_tests_colour_test_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(NKUTILS_CFLAGS) \
+	$(_NKUTILS_INTERNAL_CFLAGS)
+
+%C%_tests_colour_test_LDADD = \
+	$(NKUTILS_LIBS) \
+	$(_NKUTILS_INTERNAL_TEST_LIBS)
+
+# uuid
+%C%_tests_uuid_test_SOURCES = \
+	%D%/tests/uuid.c
+
+%C%_tests_uuid_test_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(NKUTILS_CFLAGS) \
+	$(_NKUTILS_INTERNAL_CFLAGS)
+
+%C%_tests_uuid_test_LDADD = \
+	$(NKUTILS_LIBS) \
+	$(_NKUTILS_INTERNAL_TEST_LIBS)
+
+# gtk-settings
+%C%_tests_gtk_settings_test_SOURCES = \
+	%D%/tests/gtk-settings.c
+
+%C%_tests_gtk_settings_test_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(NKUTILS_CFLAGS) \
+	$(_NKUTILS_INTERNAL_CFLAGS)
+
+%C%_tests_gtk_settings_test_LDADD = \
+	$(NKUTILS_LIBS) \
+	$(_NKUTILS_INTERNAL_TEST_LIBS)
+
+# xdg-de
+%C%_tests_xdg_de_test_SOURCES = \
+	%D%/tests/xdg-de.c
+
+%C%_tests_xdg_de_test_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(NKUTILS_CFLAGS) \
+	$(_NKUTILS_INTERNAL_CFLAGS)
+
+%C%_tests_xdg_de_test_LDADD = \
+	$(NKUTILS_LIBS) \
+	$(_NKUTILS_INTERNAL_TEST_LIBS)
+
+# xdg-theme
+%C%_tests_xdg_theme_test_SOURCES = \
+	%D%/tests/xdg-theme.c
+
+%C%_tests_xdg_theme_test_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(NKUTILS_CFLAGS) \
+	$(_NKUTILS_INTERNAL_CFLAGS)
+
+%C%_tests_xdg_theme_test_LDADD = \
+	$(NKUTILS_LIBS) \
+	$(_NKUTILS_INTERNAL_TEST_LIBS)
+
+# bindings
+%C%_tests_bindings_test_SOURCES = \
+	%D%/tests/bindings.c
+
+%C%_tests_bindings_test_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(NKUTILS_CFLAGS) \
+	$(_NKUTILS_INTERNAL_CFLAGS)
+
+%C%_tests_bindings_test_LDADD = \
+	$(NKUTILS_LIBS) \
+	$(_NKUTILS_INTERNAL_TEST_LIBS)
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/libnkutils-nolibtool.mk
@@ -0,0 +1,39 @@
+#
+# libnkutils - Miscellaneous utilities
+#
+# Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+_libnkutils_library = \
+	%D%/libnkutils.a
+
+noinst_LIBRARIES += \
+	$(_libnkutils_library)
+
+%C%_libnkutils_a_SOURCES = \
+	$(_libnkutils_sources)
+
+%C%_libnkutils_a_CFLAGS = \
+	$(AM_CFLAGS) \
+	$(NKUTILS_CFLAGS) \
+	$(_NKUTILS_INTERNAL_CFLAGS)
+
+include %D%/libnkutils-common.mk
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/libnkutils.m4
@@ -0,0 +1,110 @@
+dnl
+dnl libnkutils - Miscellaneous utilities
+dnl
+dnl Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+dnl
+dnl Permission is hereby granted, free of charge, to any person obtaining a copy
+dnl of this software and associated documentation files (the "Software"), to deal
+dnl in the Software without restriction, including without limitation the rights
+dnl to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+dnl copies of the Software, and to permit persons to whom the Software is
+dnl furnished to do so, subject to the following conditions:
+dnl
+dnl The above copyright notice and this permission notice shall be included in
+dnl all copies or substantial portions of the Software.
+dnl
+dnl THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+dnl IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+dnl FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+dnl AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+dnl LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+dnl OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+dnl THE SOFTWARE.
+dnl
+
+
+# NK_INIT([modules])
+#     modules                       A list of modules to enable (shorthand for NK_ENABLE_MODULES)
+AC_DEFUN([NK_INIT], [
+    AC_REQUIRE([AC_HEADER_STDC])
+    AC_REQUIRE([PKG_PROG_PKG_CONFIG])
+
+    nk_glib_min_version="2.40"
+    nk_xkbcommon_min_version="0.4.1"
+
+    m4_map_args_w(_NK_MODULES, [_NK_MODULE_INIT(AS_TR_SH(], [))])
+    AC_CONFIG_COMMANDS_PRE([
+        AC_CHECK_HEADERS([locale.h])
+        AS_IF([test x${ac_cv_header_string_h} != xyes], [AC_MSG_ERROR([libnkutils: string.h is required])])
+        PKG_CHECK_MODULES([_NKUTILS_INTERNAL_GLIB], [glib-2.0 >= ${nk_glib_min_version}])
+        PKG_CHECK_MODULES([_NKUTILS_INTERNAL_TEST], [gobject-2.0])
+        m4_map_args_w(_NK_MODULES, [_NK_MODULE_CHECK(], [)])
+    ])
+
+    m4_ifnblank([$1], [NK_ENABLE_MODULES([$1])])
+    AM_XSLTPROCFLAGS="${AM_XSLTPROCFLAGS} "'${NKUTILS_XSLTPROCFLAGS}'
+
+    AM_CONDITIONAL([NK_ENABLE_UUID_LIBUUID], [test x${nk_uuid_libuuid} = xyes])
+    AM_CONDITIONAL([NK_ENABLE_UUID_APR_UTIL], [test x${nk_uuid_apr_util} = xyes])
+
+    m4_define([NK_INIT])
+])
+
+# NK_ENABLE_MODULES(modules)
+#     modules  A list of modules to enable
+AC_DEFUN([NK_ENABLE_MODULES], [
+    m4_map_args_w([$1], [_NK_ENABLE_MODULE(], [)])
+])
+
+m4_define([_NK_MODULES], [uuid enum token colour gtk-settings xdg-de xdg-theme bindings])
+
+
+# auto-enable
+m4_define([_nk_dependent_enum], [token xdg-de xdg-theme bindings])
+m4_define([_nk_dependent_gtk_settings], [xdg-theme bindings])
+m4_define([_nk_dependent_xdg_de], [xdg-theme bindings])
+
+
+
+AC_DEFUN([_NK_MODULE_INIT], [
+    nk_module_][$1][_enable=no
+])
+
+AC_DEFUN([_NK_MODULE_CHECK], [
+    AM_CONDITIONAL([NK_ENABLE_]AS_TR_CPP([$1]), [test x${nk_module_]AS_TR_SH([$1])[_enable} = xyes]_NK_MODULE_CHECK_DEPENDENT([_nk_dependent_]AS_TR_SH([$1])))
+])
+
+AC_DEFUN([_NK_MODULE_CHECK_DEPENDENT], [m4_ifdef([$1], m4_map_args_w($1, [[ -o x${nk_module_]AS_TR_SH(], [)[_enable} = xyes]]))])
+
+
+AC_DEFUN([_NK_ENABLE_MODULE], [
+    m4_if(m4_index(_NK_MODULES, [$1]), [-1], [AC_MSG_ERROR([libnkutils: No ][$1][ module])])
+    m4_ifdef([_NK_]AS_TR_CPP([$1])[_CHECK], [_NK_]AS_TR_CPP([$1])[_CHECK])
+    [nk_module_]AS_TR_SH([$1])[_enable=yes]
+])
+
+# Special dependencies
+AC_DEFUN([_NK_UUID_CHECK], [
+    PKG_CHECK_MODULES([_NKUTILS_INTERNAL_UUID_LIBUUID], [uuid], [nk_uuid_libuuid=yes], [nk_uuid_libuuid=no])
+    PKG_CHECK_MODULES([_NKUTILS_INTERNAL_UUID_APR_UTIL], [apr-util-1], [nk_uuid_apr_util=yes], [
+        PKG_CHECK_MODULES([_NKUTILS_INTERNAL_UUID_APR_UTIL], [apr-util], [nk_uuid_apr_util=yes], [nk_uuid_apr_util=no])
+    ])
+    AS_IF([test x${nk_uuid_libuuid} != xyes -a x${nk_uuid_apr_util} != xyes], [AC_MSG_ERROR([libnkutils: A UUID library is required])])
+])
+
+AC_DEFUN([_NK_XDG_THEME_CHECK], [
+    PKG_CHECK_MODULES([_NKUTILS_INTERNAL_GIO], [gio-2.0])
+    PKG_CHECK_MODULES([_NKUTILS_INTERNAL_GOBJECT], [gobject-2.0])
+])
+
+AC_DEFUN([_NK_BINDINGS_CHECK], [
+    PKG_CHECK_MODULES([_NKUTILS_INTERNAL_XKBCOMMON], [xkbcommon >= ${nk_xkbcommon_min_version}])
+    PKG_CHECK_EXISTS([xkbcommon >= 0.7.0], [
+        AC_DEFINE([NK_XKBCOMMON_HAS_COMPOSE], [1], [b])
+        AC_DEFINE([NK_XKBCOMMON_HAS_CONSUMED2], [1], [b])
+    ], [
+        PKG_CHECK_EXISTS([xkbcommon >= 0.5.0], [
+            AC_DEFINE([NK_XKBCOMMON_HAS_COMPOSE], [1], [b])
+        ])
+    ])
+])
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/man/libnkutils-man.xml
@@ -0,0 +1,198 @@
+<?xml version='1.0' encoding='utf-8' ?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
+
+<!--
+  libnkutils - Miscellaneous utilities
+
+  Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+
+  This file is part of libnkutils.
+
+  libnkutils is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  libnkutils is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with libnkutils. If not, see <http://www.gnu.org/licenses/>.
+-->
+<refentry>
+    <refsect2 id="nk-token-reference-description">
+        <para>
+            References use a shell-like format: <userinput>${<replaceable>reference-name</replaceable>}</userinput>.
+            Each reference is resolved to a data that is substitued to the reference at the corresponding place in the string.
+            The <replaceable>reference-name</replaceable> can be composed of alphabetic characters, dashes (<literal>'-'</literal>) and underscores (<literal>'_'</literal>).
+        </para>
+        <para>
+            You can use an array-like syntax for some values: <userinput>${<replaceable>reference-name</replaceable>[<replaceable>key</replaceable>]}</userinput>.
+            The <replaceable>key</replaceable> can be a name or a number depending on the data pointed by <replaceable>reference-name</replaceable>.
+            You can use fallback values, substitute values or regexes on array members.
+        </para>
+        <para>
+            You can use a fallback value if the data resolves to <literal>nothing</literal>.
+            The syntax is: <userinput>${<replaceable>reference-name</replaceable>:-<replaceable>fallback-value</replaceable>}</userinput>.
+        </para>
+        <para>
+            You can use a substitute value if (and only if) the data resolves to <literal>something</literal>.
+            The syntax is: <userinput>${<replaceable>reference-name</replaceable>:+<replaceable>substitute-value</replaceable>}</userinput>.
+        </para>
+        <para>
+            You can use a substitute value if (and only if) the data resolves to <literal>nothing</literal>.
+            The syntax is: <userinput>${<replaceable>reference-name</replaceable>:!<replaceable>substitute-value</replaceable>}</userinput>.
+        </para>
+        <para>
+            You can replace parts of the data using the regex mechanism.
+            The syntax is: <userinput>${<replaceable>reference-name</replaceable>/<replaceable>regex</replaceable>/<replaceable>replacement</replaceable>}</userinput>.
+            The <replaceable>replacement</replaceable> part is optional (including the preceding forward slash). If it is omitted, matching parts of the data will simply be removed.
+            You can repeat the <userinput>/<replaceable>regex</replaceable>/<replaceable>replacement</replaceable></userinput> part to replace as many patterns as you like.
+            Each pattern is matched against the previous replacement so be careful in your order.
+        </para>
+        <para>Examples (with data and resolved string):</para>
+        <variablelist>
+            <varlistentry>
+                <term><literal>"${name} is eating a ${fruit}."</literal></term>
+                <listitem>
+                    <para><varname>name</varname> is <literal>"Bob"</literal> and <varname>fruit</varname> is <literal>"pear"</literal>.</para>
+                    <para>Resolves to: <literal>Bob is eating a pear.</literal></para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><literal>"${name} is eating a ${fruit:-banana}."</literal></term>
+                <listitem>
+                    <para><varname>name</varname> is <literal>"Bob"</literal> and <varname>fruit</varname> is <literal>"pear"</literal>.</para>
+                    <para>Resolves to: <literal>Bob is eating a pear.</literal></para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><literal>"${name} is eating a ${fruit:-banana}."</literal></term>
+                <listitem>
+                    <para><varname>name</varname> is <literal>"Bob"</literal> and <varname>fruit</varname> is <literal>nothing</literal>.</para>
+                    <para>Resolves to: <literal>Bob is eating a banana.</literal></para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><literal>"${name} is eating a ${fruit}${addition:+ cooked with }${addition}."</literal></term>
+                <listitem>
+                    <para><varname>name</varname> is <literal>"Bob"</literal>, <varname>fruit</varname> is <literal>"pear"</literal> and <varname>addition</varname> is <literal>"chocolate"</literal>.</para>
+                    <para>Resolves to: <literal>Bob is eating a pear cooked with chocolate.</literal></para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><literal>"${name} is eating a ${fruit}${addition:+ cooked with }${addition}."</literal></term>
+                <listitem>
+                    <para><varname>name</varname> is <literal>"Bob"</literal>, <varname>fruit</varname> is <literal>"pear"</literal>and <varname>addition</varname> is <literal>nothing</literal>.</para>
+                    <para>Resolves to: <literal>Bob is eating a pear.</literal></para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><literal>"${name} is eating a ${addition:! raw }${fruit}."</literal></term>
+                <listitem>
+                    <para><varname>name</varname> is <literal>"Bob"</literal>, <varname>fruit</varname> is <literal>"pear"</literal> and <varname>addition</varname> is <literal>"chocolate"</literal>.</para>
+                    <para>Resolves to: <literal>Bob is eating a pear.</literal></para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><literal>"${name} is eating a ${addition:! raw }${fruit}."</literal></term>
+                <listitem>
+                    <para><varname>name</varname> is <literal>"Bob"</literal>, <varname>fruit</varname> is <literal>"pear"</literal>and <varname>addition</varname> is <literal>nothing</literal>.</para>
+                    <para>Resolves to: <literal>Bob is eating a raw pear.</literal></para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><literal>"${name} is eating a ${fruit}${addition/^/ cooked with }."</literal></term>
+                <listitem>
+                    <para><varname>name</varname> is <literal>"Bob"</literal>, <varname>fruit</varname> is <literal>"pear"</literal> and <varname>addition</varname> is <literal>"chocolate"</literal>.</para>
+                    <para>Resolves to: <literal>Bob is eating a pear cooked with chocolate.</literal></para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><literal>"${name} is eating a ${fruit}${addition/^/ cooked with }."</literal></term>
+                <listitem>
+                    <para><varname>name</varname> is <literal>"Bob"</literal>, <varname>fruit</varname> is <literal>"pear"</literal>and <varname>addition</varname> is <literal>nothing</literal>.</para>
+                    <para>Resolves to: <literal>Bob is eating a pear.</literal></para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><literal>"${name} is eating a ${fruit}${addition/^/ cooked with /$/ from Switzerland}."</literal></term>
+                <listitem>
+                    <para><varname>name</varname> is <literal>"Bob"</literal>, <varname>fruit</varname> is <literal>"pear"</literal> and <varname>addition</varname> is <literal>"chocolate"</literal>.</para>
+                    <para>Resolves to: <literal>Bob is eating a pear cooked with chocolate from Switzerland.</literal></para>
+                </listitem>
+            </varlistentry>
+        </variablelist>
+    </refsect2>
+
+    <refsect2 id="nk-colour-type-description">
+        <title>Colour strings</title>
+
+        <para>
+            The <type>colour string</type> format is borrowed from the <ulink url="https://www.w3.org/TR/css-color-4/#hex-notation">CSS4 colour values</ulink>.
+            Hexadecimal (<literal>#</literal>-prefixed) and decimal (<literal>rgb()</literal> format) RGB(A) notations are supported.
+        </para>
+
+        <para>Here is a quick description:</para>
+        <variablelist>
+            <varlistentry>
+                <term><userinput>#<replaceable>RR</replaceable><replaceable>GG</replaceable><replaceable>BB</replaceable><optional><replaceable>AA</replaceable></optional></userinput></term>
+                <term><userinput>#<replaceable>R</replaceable><replaceable>G</replaceable><replaceable>B</replaceable><optional><replaceable>A</replaceable></optional></userinput></term>
+                <listitem>
+                    <para>Each value is an <type>hexadecimal</type> value in the <literal>0x00</literal>-<literal>0xff</literal> range.</para>
+                    <para>The alpha value is an extension of the CSS3 notation and is optional.</para>
+                    <para>The single-letter version means <emphasis>duplicating</emphasis> the character (e.g. <literal>"#123"</literal> means <literal>"#112233"</literal>).</para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><userinput>rgb(<replaceable>red</replaceable>, <replaceable>green</replaceable>, <replaceable>blue</replaceable>)</userinput></term>
+                <term><userinput>rgba(<replaceable>red</replaceable>, <replaceable>green</replaceable>, <replaceable>blue</replaceable>, <replaceable>alpha</replaceable>)</userinput></term>
+                <listitem>
+                    <para>Each colour value can be an <type>number</type> in the <literal>0</literal>-<literal>255</literal> range or a <type>percentage</type> value.</para>
+                    <para>The alpha value is a <type>floating point</type> or a <type>percentage</type> value.</para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><userinput>hsl(<replaceable>hue</replaceable>, <replaceable>saturation</replaceable>, <replaceable>lightness</replaceable>)</userinput></term>
+                <term><userinput>hsla(<replaceable>hue</replaceable>, <replaceable>saturation</replaceable>, <replaceable>lightness</replaceable>, <replaceable>alpha</replaceable>)</userinput></term>
+                <listitem>
+                    <para>Hue is an <type>angle</type> either in <type>degrees</type> (unit <userinput>deg</userinput> or no unit), <type>gradians</type> (unit <userinput>grab</userinput>), <type>radians</type> (unit <userinput>rad</userinput>) or <type>turns</type> (unit <userinput>turn</userinput>, a <type>floating point number</type> from <literal>0</literal> to <literal>1</literal>).</para>
+                    <para>The saturation and lightness values are <type>percentage</type> values.</para>
+                    <para>The alpha value is the same as the <userinput>rgba()</userinput> notation.</para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term><userinput>hwb(<replaceable>hue</replaceable>, <replaceable>whiteness</replaceable>, <replaceable>blackness</replaceable><optional>, <replaceable>alpha</replaceable></optional>)</userinput></term>
+                <listitem>
+                    <para>Hue is the same as the <userinput>hsl()</userinput> notation.</para>
+                    <para>The whiteness and blackness values are <type>percentage</type> values.</para>
+                    <para>The alpha value is the same as the <userinput>rgba()</userinput> notation.</para>
+                </listitem>
+            </varlistentry>
+        </variablelist>
+    </refsect2>
+
+    <refsect2 id="nk-bindings-type-description">
+        <title>Binding strings</title>
+
+        <para>
+            The <type>binding string</type> format supports two (and a half) notations. It can be either <literal><replaceable>Modifier</replaceable>-<replaceable>Modifier</replaceable>-<replaceable>Bind</replaceable></literal>
+            or the <ulink url="https://developer.gnome.org/gtk3/stable/gtk3-Keyboard-Accelerators.html#gtk-accelerator-parse">GTK+ accelerator format</ulink>.
+            The bind can either be a keysym name, a keycode in square brackets (e.g. <literal>[10]</literal>) or a mouse button specification (e.g. <literal>MousePrimary</literal> or <literal>MouseDPrimary</literal> for double click).
+        </para>
+    </refsect2>
+</refentry>
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/meson.build
@@ -0,0 +1,180 @@
+#
+# libnkutils - Miscellaneous utilities
+#
+# Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+project('libnkutils', 'c',
+    license: 'MIT',
+    default_options: [
+        'c_std=gnu11',
+        'warning_level=3',
+    ],
+)
+
+nk_glib_min_version = '2.40'
+
+nk_options = []
+
+nk_modules = [
+    'uuid',
+    'token',
+    'gtk-settings',
+    'xdg-de',
+    'xdg-theme',
+    'enum',
+    'colour',
+    'bindings',
+]
+nk_dependencies = [
+    [ 'token', 'enum' ],
+    [ 'xdg-de', 'enum' ],
+    [ 'xdg-theme', 'enum' ],
+    [ 'xdg-theme', 'gtk-settings' ],
+    [ 'xdg-theme', 'xdg-de' ],
+    [ 'bindings', 'enum' ],
+    [ 'bindings', 'gtk-settings' ],
+    [ 'bindings', 'xdg-de' ],
+]
+
+nk_sources = []
+nk_args = [
+    '-DSRCDIR="@0@"'.format(meson.current_source_dir()),
+    '-DSYSCONFDIR="@0@"'.format(join_paths(get_option('prefix'), get_option('sysconfdir'))),
+    '-DDATADIR="@0@"'.format(join_paths(get_option('prefix'), get_option('datadir'))),
+]
+nk_deps = [ dependency('glib-2.0', version: '>= @0@'.format(nk_glib_min_version)) ]
+
+nkutils_xsltpaths = [ join_paths(meson.current_source_dir(), 'man'), meson.current_build_dir() ]
+nkutils_manfiles = files('man/libnkutils-man.xml')
+nkutils_mandepends = []
+nkutils_docbook_conditions = []
+
+foreach m : nk_modules
+    if get_option(m)
+        nk_options += '@0@=true'.format(m)
+    endif
+    set_variable(m.underscorify(), get_option(m) or not meson.is_subproject())
+endforeach
+
+foreach d : nk_dependencies
+    if get_option(d[0])
+        set_variable(d[1].underscorify(), true)
+    endif
+endforeach
+
+foreach m : nk_modules
+    if get_variable(m.underscorify())
+        nk_sources += files(
+            'src/@0@.c'.format(m),
+            'src/nkutils-@0@.h'.format(m),
+        )
+    endif
+endforeach
+
+if uuid
+    nk_libuuid = dependency('uuid', required: false)
+    if nk_libuuid.found()
+        nk_sources += files('src/uuid-libuuid.c')
+        nk_deps += nk_libuuid
+    else
+        nk_apr_util_1 = dependency('apr-util-1', required: false)
+        if nk_apr_util_1.found()
+            nk_sources += files('src/uuid-apr-util.c')
+            nk_deps += nk_apr_util_1
+        else
+            nk_apr_util = dependency('apr-util', required: false)
+            if nk_apr_util.found()
+                nk_sources += files('src/uuid-apr-util.c')
+                nk_deps += nk_apr_util
+            else
+                error('libnkutils: A UUID library is required')
+            endif
+        endif
+    endif
+    nk_sources += files('src/uuid-internal.h')
+endif
+
+if xdg_theme
+    nk_deps += [ dependency('gio-2.0'), dependency('gobject-2.0') ]
+endif
+
+if bindings
+    nk_xkbcommon = dependency('xkbcommon', version: '>= 0.4.1')
+    nk_deps += nk_xkbcommon
+    if nk_xkbcommon.version().version_compare('>= 0.7.0')
+        nk_args += '-DNK_XKBCOMMON_HAS_COMPOSE'
+        nk_args += '-DNK_XKBCOMMON_HAS_CONSUMED2'
+    elif nk_xkbcommon.version().version_compare('>= 0.5.0')
+        nk_args += '-DNK_XKBCOMMON_HAS_COMPOSE'
+    endif
+endif
+
+nk_lib = static_library('nkutils', nk_sources, c_args: nk_args, dependencies: nk_deps)
+nk_inc = include_directories('.', 'src')
+nk_src = []
+
+if meson.is_subproject()
+    nk_git_work_tree = get_option('git-work-tree')
+else
+    nk_git_work_tree = meson.source_root()
+endif
+
+if nk_git_work_tree != ''
+    nk_options += 'git-work-tree=@0@'.format(nk_git_work_tree)
+    nk_git = find_program('git', required: false)
+    if not nk_git.found()
+        nk_git = ''
+    endif
+
+    nk_git_version = executable('nk-git-version', files('src/git-version.c'), dependencies: dependency('glib-2.0', native: true), native: true)
+    nk_src += custom_target('nkutils-git-version.h',
+        output: 'nkutils-git-version.h',
+        command: [ nk_git_version, 'header', '@OUTPUT@', nk_git_work_tree, join_paths(nk_git_work_tree, '.git'), nk_git ],
+        build_always: true,
+        build_by_default: not meson.is_subproject(),
+    )
+
+    nkutils_mandepends += custom_target('nkutils-git-version.ent',
+        output: 'nkutils-git-version.ent',
+        command: [ nk_git_version, 'entity', '@OUTPUT@', nk_git_work_tree, join_paths(nk_git_work_tree, '.git'), nk_git ],
+        build_always: true,
+        build_by_default: not meson.is_subproject(),
+    )
+endif
+
+libnkutils = declare_dependency(link_with: nk_lib, include_directories: nk_inc, dependencies: nk_deps, sources: nk_src)
+
+
+if token
+    executable('nk-token-replace', files('src/token-example.c'), dependencies: libnkutils)
+endif
+
+if xdg_theme
+    executable('nk-xdg-theme-lookup', files('src/xdg-theme-example.c'), dependencies: libnkutils)
+endif
+
+foreach m : nk_modules
+    if get_variable(m.underscorify())
+        nk_test = executable('nk-@0@.test'.format(m), files('tests/@0@.c'.format(m)), c_args: nk_args, dependencies: libnkutils)
+        test('libnkutils @0@ module tests'.format(m), nk_test)
+    endif
+endforeach
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/meson_options.txt
@@ -0,0 +1,9 @@
+option('uuid', type: 'boolean', value: false, description: 'nkutils uuid module')
+option('token', type: 'boolean', value: false, description: 'nkutils token module')
+option('gtk-settings', type: 'boolean', value: false, description: 'nkutils gtk-settings module')
+option('xdg-de', type: 'boolean', value: false, description: 'nkutils xdg-de module')
+option('xdg-theme', type: 'boolean', value: false, description: 'nkutils xdg-theme module')
+option('enum', type: 'boolean', value: false, description: 'nkutils enum module')
+option('colour', type: 'boolean', value: false, description: 'nkutils colour module')
+option('bindings', type: 'boolean', value: false, description: 'nkutils bindings module')
+option('git-work-tree', type: 'string', value: '', description: 'Git work tree directory')
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/bindings.c
@@ -0,0 +1,1012 @@
+/*
+ * libnkutils/bindings - Miscellaneous utilities, bindings module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "libnkutils-bindings"
+
+#include <errno.h>
+#include <locale.h>
+#include <string.h>
+
+#include <glib.h>
+
+#include <xkbcommon/xkbcommon.h>
+#ifdef NK_XKBCOMMON_HAS_COMPOSE
+#include <xkbcommon/xkbcommon-compose.h>
+#endif /* NK_XKBCOMMON_HAS_COMPOSE */
+
+#include "nkutils-gtk-settings.h"
+#include "nkutils-xdg-de.h"
+#include "nkutils-enum.h"
+#include "nkutils-bindings.h"
+
+#define NK_BINDINGS_DEFAULT_DOUBLE_CLICK_DELAY 200
+
+#define NK_BINDINGS_MAX_ALIASES 4 /* For Alt */
+
+#define _NK_VALUE_TO_BINDING(mask, value) ((guint64)((((guint64)(mask)) << 32) | (guint32)(value)))
+#define NK_KEYCODE_TO_BINDING(mask, keycode) _NK_VALUE_TO_BINDING(mask, keycode)
+#define NK_KEYSYM_TO_BINDING(mask, keysym) _NK_VALUE_TO_BINDING(mask, keysym)
+#define NK_BUTTON_TO_BINDING(mask, button) _NK_VALUE_TO_BINDING(mask, button)
+#define NK_SCROLL_TO_BINDING(mask, axis, step) _NK_VALUE_TO_BINDING(mask, (axis << 1) + ( (step < 0) ? 0 : 1 ))
+
+struct _NkBindings {
+    guint64 double_click_delay;
+    GList *scopes;
+    GList *seats;
+};
+
+struct _NkBindingsSeat {
+    NkBindings *bindings;
+    GList *link;
+    struct xkb_context *context;
+    struct xkb_keymap  *keymap;
+    struct xkb_state   *state;
+    xkb_mod_index_t     modifiers[NK_BINDINGS_NUM_MODIFIERS][NK_BINDINGS_MAX_ALIASES + 1];
+#ifdef NK_XKBCOMMON_HAS_COMPOSE
+    struct {
+        struct xkb_compose_table *table;
+        struct xkb_compose_state *state;
+    } compose;
+#endif /* NK_XKBCOMMON_HAS_COMPOSE */
+    GList *on_release;
+    GHashTable *last_timestamps;
+};
+
+typedef struct {
+    guint64 id;
+    GHashTable *keycodes;
+    GHashTable *keysyms;
+    GHashTable *buttons;
+    GHashTable *scroll;
+} NkBindingsScope;
+
+typedef struct {
+    NkBindingsCallback callback;
+    gpointer user_data;
+    GDestroyNotify notify;
+} NkBindingsBindingBase;
+
+typedef struct {
+    NkBindingsBindingBase base;
+} NkBindingsBindingPress;
+
+typedef struct {
+    NkBindingsBindingBase base;
+} NkBindingsBindingRelease;
+
+typedef struct {
+    guint64 binding;
+    guint64 scope;
+    NkBindingsBindingPress press;
+    NkBindingsBindingRelease release;
+} NkBindingsBinding;
+
+typedef struct {
+    NkBindingsBinding click;
+    NkBindingsBinding dclick;
+} NkBindingsBindingMouse;
+
+static const gchar * const _nk_bindings_modifiers_names[] = {
+    [NK_BINDINGS_MODIFIER_SHIFT   + NK_BINDINGS_NUM_MODIFIERS * 0] = "shift",
+    [NK_BINDINGS_MODIFIER_CONTROL + NK_BINDINGS_NUM_MODIFIERS * 0] = "control",
+    [NK_BINDINGS_MODIFIER_ALT     + NK_BINDINGS_NUM_MODIFIERS * 0] = "alt",
+    [NK_BINDINGS_MODIFIER_SUPER   + NK_BINDINGS_NUM_MODIFIERS * 0] = "super",
+    [NK_BINDINGS_MODIFIER_META    + NK_BINDINGS_NUM_MODIFIERS * 0] = "meta",
+    [NK_BINDINGS_MODIFIER_HYPER   + NK_BINDINGS_NUM_MODIFIERS * 0] = "hyper",
+    /* Allow a few aliases */
+    [NK_BINDINGS_MODIFIER_SHIFT   + NK_BINDINGS_NUM_MODIFIERS * 1] = "shift_l",
+    [NK_BINDINGS_MODIFIER_CONTROL + NK_BINDINGS_NUM_MODIFIERS * 1] = "control_l",
+    [NK_BINDINGS_MODIFIER_ALT     + NK_BINDINGS_NUM_MODIFIERS * 1] = "alt_l",
+    [NK_BINDINGS_MODIFIER_SUPER   + NK_BINDINGS_NUM_MODIFIERS * 1] = "super_l",
+    [NK_BINDINGS_MODIFIER_META    + NK_BINDINGS_NUM_MODIFIERS * 1] = "meta_l",
+    [NK_BINDINGS_MODIFIER_HYPER   + NK_BINDINGS_NUM_MODIFIERS * 1] = "hyper_l",
+    [NK_BINDINGS_MODIFIER_SHIFT   + NK_BINDINGS_NUM_MODIFIERS * 2] = "shift_r",
+    [NK_BINDINGS_MODIFIER_CONTROL + NK_BINDINGS_NUM_MODIFIERS * 2] = "control_r",
+    [NK_BINDINGS_MODIFIER_ALT     + NK_BINDINGS_NUM_MODIFIERS * 2] = "alt_r",
+    [NK_BINDINGS_MODIFIER_SUPER   + NK_BINDINGS_NUM_MODIFIERS * 2] = "super_r",
+    [NK_BINDINGS_MODIFIER_META    + NK_BINDINGS_NUM_MODIFIERS * 2] = "meta_r",
+    [NK_BINDINGS_MODIFIER_HYPER   + NK_BINDINGS_NUM_MODIFIERS * 2] = "hyper_r",
+    [NK_BINDINGS_MODIFIER_SHIFT   + NK_BINDINGS_NUM_MODIFIERS * 3] = "",
+    [NK_BINDINGS_MODIFIER_CONTROL + NK_BINDINGS_NUM_MODIFIERS * 3] = "ctrl",
+    [NK_BINDINGS_MODIFIER_ALT     + NK_BINDINGS_NUM_MODIFIERS * 3] = "altgr",
+    [NK_BINDINGS_MODIFIER_SUPER   + NK_BINDINGS_NUM_MODIFIERS * 3] = "logo",
+    [NK_BINDINGS_MODIFIER_META    + NK_BINDINGS_NUM_MODIFIERS * 3] = "",
+    [NK_BINDINGS_MODIFIER_HYPER   + NK_BINDINGS_NUM_MODIFIERS * 3] = "",
+};
+
+static const gchar * const _nk_bindings_mouse_button_names[] = {
+    [NK_BINDINGS_MOUSE_BUTTON_PRIMARY] = "Primary",
+    [NK_BINDINGS_MOUSE_BUTTON_SECONDARY] = "Secondary",
+    [NK_BINDINGS_MOUSE_BUTTON_MIDDLE] = "Middle",
+    [NK_BINDINGS_MOUSE_BUTTON_BACK] = "Back",
+    [NK_BINDINGS_MOUSE_BUTTON_FORWARD] = "Forward",
+};
+
+static const gchar * const _nk_bindings_scroll_names[] = {
+    [NK_SCROLL_TO_BINDING(0, NK_BINDINGS_SCROLL_AXIS_VERTICAL, -1)] = "Up",
+    [NK_SCROLL_TO_BINDING(0, NK_BINDINGS_SCROLL_AXIS_VERTICAL,  1)] = "Down",
+    [NK_SCROLL_TO_BINDING(0, NK_BINDINGS_SCROLL_AXIS_HORIZONTAL, -1)] = "Left",
+    [NK_SCROLL_TO_BINDING(0, NK_BINDINGS_SCROLL_AXIS_HORIZONTAL,  1)] = "Right",
+};
+
+GQuark
+nk_bindings_error(void)
+{
+    return g_quark_from_static_string("nk_bindings_error-quark");
+}
+
+static void
+_nk_bindings_scope_free(gpointer data)
+{
+    NkBindingsScope *scope = data;
+
+    g_hash_table_unref(scope->keycodes);
+    g_hash_table_unref(scope->keysyms);
+    g_hash_table_unref(scope->buttons);
+    g_hash_table_unref(scope->scroll);
+
+    g_slice_free(NkBindingsScope, scope);
+}
+
+NkBindings *
+nk_bindings_new(guint64 double_click_delay)
+{
+    NkBindings *self;
+    self = g_new0(NkBindings, 1);
+
+    switch ( nk_xdg_de_detect() )
+    {
+    case NK_XDG_DE_NONE:
+    case NK_XDG_DE_GNOME:
+    case NK_XDG_DE_I3:
+        /* Just use the GTK+ settings fallback */
+    break;
+    case NK_XDG_DE_KDE:
+    {
+        gchar *path;
+        GKeyFile *settings;
+
+        path = g_build_filename(g_get_user_config_dir(), "kcminputrc", NULL);
+        settings = g_key_file_new();
+
+        if ( g_key_file_load_from_file(settings, path, G_KEY_FILE_NONE, NULL) )
+            self->double_click_delay = g_key_file_get_uint64(settings, "KDE", "DoubleClickInterval", NULL);
+        g_key_file_free(settings);
+        g_free(path);
+    }
+    break;
+    }
+
+    /* Always fallback to the GTK+ settings */
+    const gchar *gtk_settings_keys[NK_GTK_SETTINGS_NUM_VERSION] = {
+        "gtk-double-click-time",
+        "gtk-double-click-time",
+    };
+    if ( self->double_click_delay < 1 )
+        nk_gtk_settings_get_uint64(&self->double_click_delay, gtk_settings_keys);
+
+    /* If nothing better, use the application value */
+    if ( self->double_click_delay < 1 )
+        self->double_click_delay = double_click_delay;
+
+    /* If nothing better, use the application value */
+    if ( self->double_click_delay < 1 )
+        self->double_click_delay = NK_BINDINGS_DEFAULT_DOUBLE_CLICK_DELAY;
+
+    return self;
+}
+
+static void _nk_bindings_seat_free(gpointer data);
+void
+nk_bindings_free(NkBindings *self)
+{
+    if ( self == NULL )
+        return;
+
+    g_list_free_full(self->seats, _nk_bindings_seat_free);
+
+    nk_bindings_reset_bindings(self);
+
+    g_free(self);
+}
+
+static void
+_nk_bindings_binding_notify(NkBindingsBinding *binding)
+{
+    if ( binding->press.base.notify != NULL )
+        binding->press.base.notify(binding->press.base.user_data);
+    if ( binding->release.base.notify != NULL )
+        binding->release.base.notify(binding->release.base.user_data);
+}
+
+static void
+_nk_bindings_binding_free(gpointer data)
+{
+    NkBindingsBinding *binding = data;
+
+    _nk_bindings_binding_notify(binding);
+
+    g_slice_free(NkBindingsBinding, binding);
+}
+
+static void
+_nk_bindings_binding_mouse_free(gpointer data)
+{
+    NkBindingsBindingMouse *binding = data;
+
+    _nk_bindings_binding_notify(&binding->click);
+    _nk_bindings_binding_notify(&binding->dclick);
+
+    g_slice_free(NkBindingsBindingMouse, binding);
+}
+
+static gint
+_nk_bindings_scope_compare(gconstpointer a, gconstpointer b)
+{
+    const NkBindingsScope *sa = a, *sb = b;
+    return ( sb->id - sa->id );
+}
+
+static NkBindingsScope *
+_nk_bindings_get_scope(NkBindings *self, guint64 scope_id)
+{
+    GList *link;
+    NkBindingsScope *scope, cscope = { .id = scope_id };
+    link = g_list_find_custom(self->scopes, &cscope, _nk_bindings_scope_compare);
+    if ( link != NULL )
+        scope = link->data;
+    else
+    {
+        scope = g_slice_new(NkBindingsScope);
+        scope->id = scope_id;
+        scope->keycodes = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, _nk_bindings_binding_free);
+        scope->keysyms = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, _nk_bindings_binding_free);
+        scope->buttons = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, _nk_bindings_binding_mouse_free);
+        scope->scroll = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, _nk_bindings_binding_free);
+        self->scopes = g_list_insert_sorted(self->scopes, scope, _nk_bindings_scope_compare);
+    }
+
+    return scope;
+}
+
+static gboolean
+_nk_bindings_parse_modifier(const gchar *string, xkb_mod_mask_t *mask)
+{
+    guint64 value;
+
+    if ( ! nk_enum_parse(string, _nk_bindings_modifiers_names, G_N_ELEMENTS(_nk_bindings_modifiers_names), TRUE, FALSE, &value) )
+        return FALSE;
+
+    value %= NK_BINDINGS_NUM_MODIFIERS;
+
+    *mask |= (1 << value);
+    return TRUE;
+}
+
+gboolean
+nk_bindings_add_binding(NkBindings *self, guint64 scope_id, const gchar *string, NkBindingsCallback callback, gpointer user_data, GDestroyNotify notify, GError **error)
+{
+    g_return_val_if_fail(self != NULL, FALSE);
+    g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
+
+    gboolean on_release = FALSE;
+    xkb_mod_mask_t last_mask = 0;
+    xkb_mod_mask_t mask = 0;
+    xkb_keysym_t last_keysym = XKB_KEY_NoSymbol;
+    xkb_keysym_t keysym = XKB_KEY_NoSymbol;
+
+    const gchar *w = string;
+
+    enum {
+        NK_BINDINGS_MODE_NONE,
+        NK_BINDINGS_MODE_EXCLAMATION  = '!',
+        NK_BINDINGS_MODE_GTK   = '>',
+        NK_BINDINGS_MODE_PLUS  = '+',
+        NK_BINDINGS_MODE_MINUS = '-',
+    } mode = NK_BINDINGS_MODE_NONE;
+
+    if ( g_utf8_get_char(w) == '!' )
+    {
+        w = g_utf8_next_char(w);
+        on_release = TRUE;
+        mode = NK_BINDINGS_MODE_EXCLAMATION;
+    }
+
+    gsize l;
+    gchar *tmp;
+    const gchar *e, *s;
+    l = strlen(w);
+    tmp = g_newa(gchar, l + 1);
+    e = w + l;
+    s = w;
+    for ( e = w + l ; w < e ; w = g_utf8_next_char(w) )
+    {
+        gunichar wc = g_utf8_get_char(w);
+        switch ( wc )
+        {
+        case '!':
+            g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': '!' must be the first character", string);
+            return FALSE;
+        case '<':
+            if ( ( mode != NK_BINDINGS_MODE_NONE ) && ( mode != NK_BINDINGS_MODE_GTK ) )
+            {
+                g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': you cannot mix syntaxes", string);
+                return FALSE;
+            }
+            mode = NK_BINDINGS_MODE_GTK;
+            s = g_utf8_next_char(w);
+            continue;
+        case '>':
+        case '+':
+        case '-':
+            if ( mode == NK_BINDINGS_MODE_NONE )
+            {
+                if ( wc == '>' )
+                {
+                    g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': expected identifier or '<', got '>'", string);
+                    return FALSE;
+                }
+                mode = wc;
+            }
+            else if ( mode == NK_BINDINGS_MODE_EXCLAMATION )
+            {
+                if ( wc != '>' )
+                    mode = wc;
+            }
+            if ( mode != wc )
+            {
+                g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': you cannot mix syntaxes", string);
+                return FALSE;
+            }
+            if ( s == w )
+            {
+                g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': cannot have an empty modifier", string);
+                return FALSE;
+            }
+
+            g_snprintf(tmp, w - s + 1, "%s", s);
+            if ( mode != NK_BINDINGS_MODE_GTK )
+            {
+                last_mask = mask;
+                last_keysym = xkb_keysym_from_name(tmp, XKB_KEYSYM_NO_FLAGS);
+            }
+            if ( ! _nk_bindings_parse_modifier(tmp, &mask) )
+            {
+                if ( g_ascii_strcasecmp(tmp, "release") == 0 )
+                {
+                    if ( on_release )
+                    {
+                        g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': cannot have on-release syntax twice", string);
+                        return FALSE;
+                    }
+                    on_release = TRUE;
+                }
+                else
+                {
+                    g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': cannot have two non-modifiers", string);
+                    return FALSE;
+                }
+            }
+            s = g_utf8_next_char(w);
+        break;
+        case '[':
+            w = g_utf8_prev_char(e);
+        break;
+        default:
+            if ( g_unichar_isalnum(wc) || ( wc == '_' ) )
+                break;
+            g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': unexpected character '%.*s'", string, (gint) ( g_utf8_next_char(w) - w ),  w);
+            return FALSE;
+        }
+    }
+
+    NkBindingsScope *scope;
+    scope = _nk_bindings_get_scope(self, scope_id);
+
+    NkBindingsBinding *binding = NULL;
+    if ( s < e )
+    {
+        if ( g_utf8_get_char(s) == '[' )
+        {
+            s = g_utf8_next_char(s);
+
+            guint64 keycode;
+            gchar *ce;
+            errno = 0;
+            keycode = g_ascii_strtoull(s, &ce, 10);
+            if ( s == ce )
+            {
+                g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': could not parse keycode", string);
+                return FALSE;
+            }
+            else if ( ( g_utf8_get_char(ce) != ']' ) || ( e != g_utf8_next_char(ce) ) )
+            {
+                g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': keycode must be the end of the binding string, enclosed in squared brackets '[]'", string);
+                return FALSE;
+            }
+            else if ( ! xkb_keycode_is_legal_ext(keycode) )
+            {
+                g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': wrong keycode value %"G_GINT64_MODIFIER"u", string, keycode);
+                return FALSE;
+            }
+
+            guint64 value = NK_KEYCODE_TO_BINDING(mask, keycode);
+            binding = g_hash_table_lookup(scope->keycodes, &value);
+            if ( binding == NULL )
+            {
+                binding = g_slice_new0(NkBindingsBinding);
+                binding->binding = value;
+                g_hash_table_insert(scope->keycodes, &binding->binding, binding);
+            }
+        }
+        else if ( g_ascii_strncasecmp(s, "Mouse", strlen("Mouse")) == 0 )
+        {
+            s += strlen("Mouse");
+
+            gboolean double_click = FALSE;
+            if ( g_unichar_toupper(g_utf8_get_char(s)) == 'D' )
+            {
+                s = g_utf8_next_char(s);
+                double_click = TRUE;
+            }
+
+            guint64 button;
+            if ( g_ascii_strncasecmp(s, "Extra", strlen("Extra")) == 0 )
+            {
+                s += strlen("Extra");
+                gchar *ce;
+                errno = 0;
+                button = g_ascii_strtoull(s, &ce, 10);
+                if ( s == ce )
+                {
+                    g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': could not parse mouse extra button number", string);
+                    return FALSE;
+                }
+                else if ( e != ce )
+                {
+                    g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': mouse extra button must be the end of the binding string", string);
+                    return FALSE;
+                }
+                button += NK_BINDINGS_MOUSE_BUTTON_EXTRA;
+            }
+            else if ( ! nk_enum_parse(s, _nk_bindings_mouse_button_names, G_N_ELEMENTS(_nk_bindings_mouse_button_names), TRUE, FALSE, &button) )
+            {
+                g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': unknown mouse button %s", string, s);
+                return FALSE;
+            }
+
+            guint64 value = NK_BUTTON_TO_BINDING(mask, button);
+            NkBindingsBindingMouse *mouse_binding;
+            mouse_binding = g_hash_table_lookup(scope->buttons, &value);
+            if ( mouse_binding == NULL )
+            {
+                mouse_binding = g_slice_new0(NkBindingsBindingMouse);
+                mouse_binding->click.binding = value;
+                mouse_binding->dclick.binding = value;
+                g_hash_table_insert(scope->buttons, &mouse_binding->click.binding, mouse_binding);
+            }
+
+            if ( double_click )
+                binding = &mouse_binding->dclick;
+            else
+                binding = &mouse_binding->click;
+        }
+        else if ( g_ascii_strncasecmp(s, "Scroll", strlen("Scroll")) == 0 )
+        {
+            s += strlen("Scroll");
+
+            if ( on_release )
+            {
+                g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': scroll action cannot be bound on release", string);
+                return FALSE;
+            }
+
+            guint64 scroll;
+            if ( ! nk_enum_parse(s, _nk_bindings_scroll_names, G_N_ELEMENTS(_nk_bindings_scroll_names), TRUE, FALSE, &scroll) )
+            {
+                g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_SYNTAX, "Syntax error in binding '%s': unknown scroll direction %s", string, s);
+                return FALSE;
+            }
+
+            guint64 value = _NK_VALUE_TO_BINDING(mask, scroll);
+            binding = g_hash_table_lookup(scope->scroll, &value);
+            if ( binding == NULL )
+            {
+                binding = g_slice_new0(NkBindingsBinding);
+                binding->binding = value;
+                g_hash_table_insert(scope->scroll, &binding->binding, binding);
+            }
+        }
+        else
+            keysym = xkb_keysym_from_name(s, XKB_KEYSYM_NO_FLAGS);
+    }
+
+    if ( binding == NULL )
+    {
+        if ( keysym == XKB_KEY_NoSymbol )
+        {
+            if  ( last_keysym == XKB_KEY_NoSymbol )
+            {
+                g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_ALREADY_REGISTERED, "No keysym found for binding '%s'", string);
+                return FALSE;
+            }
+            keysym = last_keysym;
+            mask = last_mask;
+        }
+        guint64 value = NK_KEYSYM_TO_BINDING(mask, keysym);
+        binding = g_hash_table_lookup(scope->keysyms, &value);
+        if ( binding == NULL )
+        {
+            binding = g_slice_new0(NkBindingsBinding);
+            binding->binding = value;
+            g_hash_table_insert(scope->keysyms, &binding->binding, binding);
+        }
+    }
+
+    NkBindingsBindingBase *base = on_release ? &binding->release.base : &binding->press.base;
+
+    if ( base->callback != NULL )
+    {
+        g_set_error(error, NK_BINDINGS_ERROR, NK_BINDINGS_ERROR_ALREADY_REGISTERED, "There is already a binding matching '%s'", string);
+        return FALSE;
+    }
+
+    binding->scope = scope_id;
+    base->callback = callback;
+    base->user_data = user_data;
+    base->notify = notify;
+
+    return TRUE;
+}
+
+void
+nk_bindings_reset_bindings(NkBindings *self)
+{
+    g_list_free_full(self->scopes, _nk_bindings_scope_free);
+    self->scopes = NULL;
+}
+
+static gboolean
+_nk_bindings_seat_binding_trigger(NkBindingsSeat *self, NkBindingsBinding *binding, gpointer target, gboolean trigger)
+{
+    if ( binding == NULL )
+        return FALSE;
+
+    gboolean handled = FALSE;
+    gboolean has_press = ( binding->press.base.callback != NULL );
+    gboolean on_release_waiting = ( g_list_find(self->on_release, binding) != NULL );
+    if ( trigger && has_press )
+        handled = binding->press.base.callback(binding->scope, target, binding->press.base.user_data);
+    if ( ( ! on_release_waiting ) && ( binding->release.base.callback != NULL ) && ( handled || ( ! trigger ) || ( ! has_press ) ) )
+    {
+        self->on_release = g_list_prepend(self->on_release, binding);
+        on_release_waiting = TRUE;
+    }
+
+    return ( handled || on_release_waiting );
+}
+
+static gboolean
+_nk_bindings_try_key_bindings(NkBindings *self, NkBindingsSeat *seat, gpointer target, guint64 keycode, guint64 keysym, gboolean trigger)
+{
+    GList *scope_;
+    for ( scope_ = self->scopes ; scope_ != NULL ; scope_ = g_list_next(scope_) )
+    {
+        NkBindingsScope *scope = scope_->data;
+        if ( _nk_bindings_seat_binding_trigger(seat, g_hash_table_lookup(scope->keycodes, &keycode), target, trigger) )
+            return TRUE;
+        if ( ( keysym != XKB_KEY_NoSymbol ) && _nk_bindings_seat_binding_trigger(seat, g_hash_table_lookup(scope->keysyms, &keysym), target, trigger) )
+            return TRUE;
+    }
+
+    return FALSE;
+}
+
+static gboolean
+_nk_bindings_try_button_bindings(NkBindings *self, NkBindingsSeat *seat, gpointer target, guint64 **button, guint64 time)
+{
+    NkBindingsBindingMouse *mouse_binding = NULL;
+    GList *scope_;
+    for ( scope_ = self->scopes ; scope_ != NULL ; scope_ = g_list_next(scope_) )
+    {
+        NkBindingsScope *scope = scope_->data;
+
+        mouse_binding = g_hash_table_lookup(scope->buttons, *button);
+        if ( mouse_binding == NULL )
+            continue;
+
+        if ( time < self->double_click_delay )
+        {
+            if ( _nk_bindings_seat_binding_trigger(seat, &mouse_binding->dclick, target, TRUE) )
+            {
+                *button = &mouse_binding->click.binding;
+                return TRUE;
+            }
+        }
+        if ( _nk_bindings_seat_binding_trigger(seat, &mouse_binding->click, target, TRUE) )
+        {
+            *button = &mouse_binding->click.binding;
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+static NkBindingsBinding *
+_nk_bindings_try_scroll_bindings(NkBindings *self, NkBindingsSeat *seat, gpointer target, guint64 scroll)
+{
+    GList *scope_;
+    for ( scope_ = self->scopes ; scope_ != NULL ; scope_ = g_list_next(scope_) )
+    {
+        NkBindingsScope *scope = scope_->data;
+        NkBindingsBinding *binding;
+
+        binding = g_hash_table_lookup(scope->scroll, &scroll);
+        if ( _nk_bindings_seat_binding_trigger(seat, binding, target, TRUE) )
+            return binding;
+    }
+
+    return NULL;
+}
+
+NkBindingsSeat *
+nk_bindings_seat_new(NkBindings *bindings, enum xkb_context_flags flags)
+{
+    struct xkb_context *context;
+    NkBindingsSeat *self;
+
+    context = xkb_context_new(flags);
+    if ( context == NULL )
+        return NULL;
+
+    self = g_new0(NkBindingsSeat, 1);
+    self->bindings = bindings;
+    self->context = context;
+
+#ifdef NK_XKBCOMMON_HAS_COMPOSE
+    self->compose.table = xkb_compose_table_new_from_locale(self->context, setlocale(LC_CTYPE, NULL), 0);
+    if ( self->compose.table != NULL )
+        self->compose.state = xkb_compose_state_new(self->compose.table, 0);
+#endif /* NK_XKBCOMMON_HAS_COMPOSE */
+
+    self->last_timestamps = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, g_free);
+    return self;
+}
+
+static void _nk_bindings_seat_free_on_release(NkBindingsSeat *self, gpointer target, gboolean trigger);
+static void
+_nk_bindings_seat_free(gpointer data)
+{
+    NkBindingsSeat *self = data;
+
+    _nk_bindings_seat_free_on_release(self, NULL, FALSE);
+
+    xkb_keymap_unref(self->keymap);
+    xkb_state_unref(self->state);
+
+    xkb_context_unref(self->context);
+
+    g_free(self);
+}
+
+void
+nk_bindings_seat_free(NkBindingsSeat *self)
+{
+    if ( self == NULL )
+        return;
+
+    self->bindings->seats = g_list_delete_link(self->bindings->seats, self->link);
+    _nk_bindings_seat_free(self);
+}
+
+static void
+_nk_bindings_seat_find_modifier(NkBindingsSeat *self, NkBindingsModifiers modifier, ...)
+{
+    va_list names;
+    const gchar *name;
+    xkb_mod_index_t i, *m = self->modifiers[modifier];
+    va_start(names, modifier);
+    while ( ( name = va_arg(names, const gchar *) ) != NULL )
+    {
+        i = xkb_keymap_mod_get_index(self->keymap, name);
+        if ( i != XKB_MOD_INVALID )
+            *m++ = i;
+    }
+    *m = XKB_MOD_INVALID;
+    va_end(names);
+}
+
+void
+nk_bindings_seat_update_keymap(NkBindingsSeat *self, struct xkb_keymap *keymap, struct xkb_state *state)
+{
+    g_return_if_fail(self != NULL);
+    g_return_if_fail((keymap != NULL && state != NULL) || (keymap == NULL && state == NULL));
+
+    xkb_keymap_unref(self->keymap);
+    xkb_state_unref(self->state);
+
+    if ( keymap == NULL )
+    {
+        nk_bindings_seat_reset(self);
+        self->keymap = NULL;
+        self->state = NULL;
+        gsize i;
+        for ( i = 0 ; i < NK_BINDINGS_NUM_MODIFIERS ; ++i )
+            self->modifiers[i][0] = XKB_MOD_INVALID;
+        return;
+    }
+
+    self->keymap = xkb_keymap_ref(keymap);
+    self->state = xkb_state_ref(state);
+
+    _nk_bindings_seat_find_modifier(self, NK_BINDINGS_MODIFIER_SHIFT, XKB_MOD_NAME_SHIFT, NULL);
+    _nk_bindings_seat_find_modifier(self, NK_BINDINGS_MODIFIER_CONTROL, XKB_MOD_NAME_CTRL, NULL);
+    _nk_bindings_seat_find_modifier(self, NK_BINDINGS_MODIFIER_ALT, XKB_MOD_NAME_ALT, "Alt", "LAlt", "RAlt", NULL);
+    _nk_bindings_seat_find_modifier(self, NK_BINDINGS_MODIFIER_META, "Meta", NULL);
+    _nk_bindings_seat_find_modifier(self, NK_BINDINGS_MODIFIER_SUPER, XKB_MOD_NAME_LOGO, "Super", NULL);
+    _nk_bindings_seat_find_modifier(self, NK_BINDINGS_MODIFIER_HYPER, "Hyper", NULL);
+}
+
+struct xkb_context *
+nk_bindings_seat_get_context(NkBindingsSeat *self)
+{
+    return self->context;
+}
+
+
+static void
+_nk_bindings_seat_get_modifiers_masks(NkBindingsSeat *self, xkb_keycode_t key, xkb_mod_mask_t *effective, xkb_mod_mask_t *not_consumed)
+{
+    *effective = 0;
+    *not_consumed = 0;
+
+    if ( self->state == NULL )
+        return;
+
+    NkBindingsModifiers mod;
+    xkb_mod_index_t *i;
+    for ( mod = 0 ; mod < NK_BINDINGS_NUM_MODIFIERS ; ++mod )
+    {
+        for ( i = self->modifiers[mod] ; *i != XKB_MOD_INVALID ; ++i )
+        {
+            if ( ! xkb_state_mod_index_is_active(self->state, *i, XKB_STATE_MODS_EFFECTIVE) )
+                continue;
+            *effective |= (1 << mod);
+#ifdef NK_XKBCOMMON_HAS_CONSUMED2
+            if ( xkb_state_mod_index_is_consumed2(self->state, key, *i, XKB_CONSUMED_MODE_GTK) != 1 )
+#else /* ! NK_XKBCOMMON_HAS_COMSUMED2 */
+            if ( xkb_state_mod_index_is_consumed(self->state, key, *i) != 1 )
+#endif /* ! NK_XKBCOMMON_HAS_COMSUMED2 */
+                *not_consumed |= (1 << mod);
+
+            break;
+        }
+    }
+}
+
+static void
+_nk_bindings_seat_free_on_release(NkBindingsSeat *self, gpointer target, gboolean trigger)
+{
+    if ( self->on_release == NULL )
+        return;
+
+    GList *link, *next;
+    for ( link = self->on_release, next = g_list_next(link) ; link != NULL ; link = next, next = g_list_next(link) )
+    {
+        NkBindingsBinding *binding = link->data;
+        if ( trigger )
+            binding->release.base.callback(binding->scope, target, binding->release.base.user_data);
+        g_list_free_1(link);
+    }
+    self->on_release = NULL;
+}
+
+gchar *
+nk_bindings_seat_handle_key(NkBindingsSeat *self, gpointer target, xkb_keycode_t keycode, NkBindingsKeyState state)
+{
+    g_return_val_if_fail(self != NULL, NULL);
+    g_return_val_if_fail(self->keymap != NULL, NULL);
+    g_return_val_if_fail(self->state != NULL, NULL);
+
+    xkb_keysym_t keysym;
+    gchar *tmp = NULL;
+    gsize length = 0;
+
+    if ( state == NK_BINDINGS_KEY_STATE_RELEASE )
+    {
+        xkb_mod_mask_t dummy, mask;
+        _nk_bindings_seat_get_modifiers_masks(self, 0, &dummy, &mask);
+        if ( mask == 0 )
+            _nk_bindings_seat_free_on_release(self, target, TRUE);
+        return NULL;
+    }
+
+    keysym = xkb_state_key_get_one_sym(self->state, keycode);
+
+    gboolean regular_press = ( state == NK_BINDINGS_KEY_STATE_PRESS );
+
+#ifdef NK_XKBCOMMON_HAS_COMPOSE
+    if ( regular_press && ( self->compose.state != NULL ) && ( keysym != XKB_KEY_NoSymbol ) && ( xkb_compose_state_feed(self->compose.state, keysym) == XKB_COMPOSE_FEED_ACCEPTED ) )
+    {
+        switch ( xkb_compose_state_get_status(self->compose.state) )
+        {
+        case XKB_COMPOSE_CANCELLED:
+            /* Eat the keysym that cancelled the compose sequence.
+             * This is default behaviour with Xlib */
+        case XKB_COMPOSE_COMPOSING:
+            return NULL;
+        case XKB_COMPOSE_COMPOSED:
+            length = xkb_compose_state_get_utf8(self->compose.state, NULL, 0);
+            if ( length != 0 )
+            {
+                tmp = g_new(gchar, length + 1);
+                length = xkb_compose_state_get_utf8(self->compose.state, tmp, length + 1);
+            }
+            return tmp;
+        case XKB_COMPOSE_NOTHING:
+        break;
+        }
+    }
+#endif /* NK_XKBCOMMON_HAS_COMPOSE */
+
+    xkb_mod_mask_t effective, not_consumed;
+    _nk_bindings_seat_get_modifiers_masks(self, keycode, &effective, &not_consumed);
+
+    if ( _nk_bindings_try_key_bindings(self->bindings, self, target, NK_KEYCODE_TO_BINDING(effective, keycode), NK_KEYSYM_TO_BINDING(not_consumed, keysym), regular_press) )
+        return NULL;
+
+    if ( ! regular_press )
+        return NULL;
+
+    if ( length == 0 )
+    {
+        length = xkb_state_key_get_utf8(self->state, keycode, NULL, 0);
+        if ( length != 0 )
+        {
+            tmp = g_new(gchar, length + 1);
+            length = xkb_state_key_get_utf8(self->state, keycode, tmp, length + 1);
+        }
+    }
+
+    return tmp;
+}
+
+gchar *
+nk_bindings_seat_handle_key_with_modmask(NkBindingsSeat *self, gpointer target, xkb_mod_mask_t modmask, xkb_keycode_t keycode, NkBindingsKeyState state)
+{
+    g_return_val_if_fail(self != NULL, NULL);
+    g_return_val_if_fail(self->keymap != NULL, NULL);
+    g_return_val_if_fail(self->state != NULL, NULL);
+
+    xkb_mod_mask_t effective_mods, depressed_mods, latched_mods, locked_mods;
+    xkb_layout_index_t depressed_layout, latched_layout, locked_layout;
+    gchar *ret;
+
+    effective_mods = xkb_state_serialize_mods(self->state, XKB_STATE_MODS_EFFECTIVE);
+    depressed_mods = xkb_state_serialize_mods(self->state, XKB_STATE_MODS_DEPRESSED);
+    latched_mods = xkb_state_serialize_mods(self->state, XKB_STATE_MODS_LATCHED);
+    locked_mods = xkb_state_serialize_mods(self->state, XKB_STATE_MODS_LOCKED);
+    depressed_layout = xkb_state_serialize_layout(self->state, XKB_STATE_LAYOUT_DEPRESSED);
+    latched_layout = xkb_state_serialize_layout(self->state, XKB_STATE_LAYOUT_LATCHED);
+    locked_layout = xkb_state_serialize_layout(self->state, XKB_STATE_LAYOUT_LOCKED);
+
+    if ( effective_mods == modmask )
+        return nk_bindings_seat_handle_key(self, target, keycode, state);
+
+    xkb_state_update_mask(self->state, modmask, 0, 0, depressed_layout, latched_layout, locked_layout);
+    ret = nk_bindings_seat_handle_key(self, target, keycode, state);
+    xkb_state_update_mask(self->state, depressed_mods, latched_mods, locked_mods, depressed_layout, latched_layout, locked_layout);
+
+    return ret;
+}
+
+gboolean
+nk_bindings_seat_handle_button(NkBindingsSeat *self, gpointer target, NkBindingsMouseButton button, NkBindingsButtonState state, guint64 timestamp)
+{
+    g_return_val_if_fail(self != NULL, FALSE);
+
+    xkb_mod_mask_t dummy, mask;
+
+    _nk_bindings_seat_get_modifiers_masks(self, 0, &dummy, &mask);
+
+    if ( state == NK_BINDINGS_BUTTON_STATE_RELEASE )
+    {
+        if ( mask == 0 )
+            _nk_bindings_seat_free_on_release(self, target, TRUE);
+        return TRUE;
+    }
+
+    guint64 binding = NK_BUTTON_TO_BINDING(mask, button), *binding_ = &binding;
+    guint64 *last_timestamp, time = timestamp;
+
+    last_timestamp = g_hash_table_lookup(self->last_timestamps, &binding);
+    if ( last_timestamp != NULL )
+        time -= *last_timestamp;
+    if ( ! _nk_bindings_try_button_bindings(self->bindings, self, target, &binding_, time) )
+        return FALSE;
+    if ( last_timestamp == NULL )
+        g_hash_table_insert(self->last_timestamps, binding_, g_memdup(&timestamp, sizeof(guint64)));
+    else
+        *last_timestamp = timestamp;
+    return TRUE;
+}
+
+gboolean
+nk_bindings_seat_handle_scroll(NkBindingsSeat *self, gpointer target, NkBindingsScrollAxis axis, gint32 step)
+{
+    g_return_val_if_fail(self != NULL, FALSE);
+    g_return_val_if_fail(step != 0, FALSE);
+
+    xkb_mod_mask_t dummy, mask;
+
+    _nk_bindings_seat_get_modifiers_masks(self, 0, &dummy, &mask);
+
+    guint64 binding_ = NK_SCROLL_TO_BINDING(mask, axis, step);
+
+    NkBindingsBinding *binding;
+    binding = _nk_bindings_try_scroll_bindings(self->bindings, self, target, binding_);
+
+    if ( binding == NULL )
+        return FALSE;
+
+    for ( step = ABS(step) ; step > 1 ; --step )
+        _nk_bindings_seat_binding_trigger(self, binding, target, TRUE);
+    return TRUE;
+}
+
+void
+nk_bindings_seat_update_mask(NkBindingsSeat *self, gpointer target, xkb_mod_mask_t depressed_mods, xkb_mod_mask_t latched_mods, xkb_mod_mask_t locked_mods, xkb_layout_index_t depressed_layout, xkb_layout_index_t latched_layout, xkb_layout_index_t locked_layout)
+{
+    g_return_if_fail(self != NULL);
+    g_return_if_fail(self->state != NULL);
+    g_return_if_fail(self->keymap != NULL);
+
+    enum xkb_state_component changed;
+
+    changed = xkb_state_update_mask(self->state, depressed_mods, latched_mods, locked_mods, depressed_layout, latched_layout, locked_layout);
+
+    if ( changed & XKB_STATE_MODS_EFFECTIVE )
+    {
+        xkb_mod_mask_t dummy, mask;
+        _nk_bindings_seat_get_modifiers_masks(self, 0, &dummy, &mask);
+        if ( mask == 0 )
+            _nk_bindings_seat_free_on_release(self, target, TRUE);
+    }
+}
+
+void
+nk_bindings_seat_reset(NkBindingsSeat *self)
+{
+    g_return_if_fail(self != NULL);
+
+    _nk_bindings_seat_free_on_release(self, NULL, FALSE);
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/colour.c
@@ -0,0 +1,652 @@
+/*
+ * libnkutils/colour - Miscellaneous utilities, colour module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "libnkutils-colour"
+
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "nkutils-colour.h"
+
+#define DMODULO(d, m) ( (d) - (guint64) (d) + ( (guint64) (d) % (m) ) )
+
+#define RGB_TO_COLOUR(r, g, b) { .red = (gdouble) (r) / 255., .green = (gdouble) (g) / 255., .blue = (gdouble) (b) / 255., .alpha = 1. }
+
+static GScanner *_nk_colour_scanner = NULL;
+
+typedef enum {
+    NK_COLOUR_SCOPE_BASE,
+    NK_COLOUR_SCOPE_ANGLES,
+} NkColourScope;
+
+typedef enum {
+    NK_COLOUR_SYMBOL_BASE_RGB,
+    NK_COLOUR_SYMBOL_BASE_RGBA,
+    NK_COLOUR_SYMBOL_BASE_HSL,
+    NK_COLOUR_SYMBOL_BASE_HSLA,
+    NK_COLOUR_SYMBOL_BASE_HWB,
+} NkColourSymbolBase;
+
+typedef enum {
+    NK_COLOUR_ANGLE_DEG,
+    NK_COLOUR_ANGLE_GRAD,
+    NK_COLOUR_ANGLE_RAD,
+    NK_COLOUR_ANGLE_TURN,
+} NkColourAngle;
+
+static const gchar * const _nk_colour_scanner_symbols_base[] = {
+    [NK_COLOUR_SYMBOL_BASE_RGB] = "rgb",
+    [NK_COLOUR_SYMBOL_BASE_RGBA] = "rgba",
+    [NK_COLOUR_SYMBOL_BASE_HSL] = "hsl",
+    [NK_COLOUR_SYMBOL_BASE_HSLA] = "hsla",
+    [NK_COLOUR_SYMBOL_BASE_HWB] = "hwb",
+};
+
+static const gchar * const _nk_colour_scanner_symbols_angle_units[] = {
+    [NK_COLOUR_ANGLE_DEG] = "deg",
+    [NK_COLOUR_ANGLE_GRAD] = "grad",
+    [NK_COLOUR_ANGLE_RAD] = "rad",
+    [NK_COLOUR_ANGLE_TURN] = "turn",
+};
+
+static const struct {
+    const gchar *name;
+    NkColour value;
+} _nk_colour_scanner_named_colours[] = {
+    { "transparent", { .red = 0., .green = 0., .blue = 0., .alpha = 0. } },
+    { "aliceblue", RGB_TO_COLOUR(240, 248, 255) },
+    { "antiquewhite", RGB_TO_COLOUR(250, 235, 215) },
+    { "aqua", RGB_TO_COLOUR(0, 255, 255) },
+    { "aquamarine", RGB_TO_COLOUR(127, 255, 212) },
+    { "azure", RGB_TO_COLOUR(240, 255, 255) },
+    { "beige", RGB_TO_COLOUR(245, 245, 220) },
+    { "bisque", RGB_TO_COLOUR(255, 228, 196) },
+    { "black", RGB_TO_COLOUR(0, 0, 0) },
+    { "blanchedalmond", RGB_TO_COLOUR(255, 235, 205) },
+    { "blue", RGB_TO_COLOUR(0, 0, 255) },
+    { "blueviolet", RGB_TO_COLOUR(138, 43, 226) },
+    { "brown", RGB_TO_COLOUR(165, 42, 42) },
+    { "burlywood", RGB_TO_COLOUR(222, 184, 135) },
+    { "cadetblue", RGB_TO_COLOUR(95, 158, 160) },
+    { "chartreuse", RGB_TO_COLOUR(127, 255, 0) },
+    { "chocolate", RGB_TO_COLOUR(210, 105, 30) },
+    { "coral", RGB_TO_COLOUR(255, 127, 80) },
+    { "cornflowerblue", RGB_TO_COLOUR(100, 149, 237) },
+    { "cornsilk", RGB_TO_COLOUR(255, 248, 220) },
+    { "crimson", RGB_TO_COLOUR(220, 20, 60) },
+    { "cyan", RGB_TO_COLOUR(0, 255, 255) },
+    { "darkblue", RGB_TO_COLOUR(0, 0, 139) },
+    { "darkcyan", RGB_TO_COLOUR(0, 139, 139) },
+    { "darkgoldenrod", RGB_TO_COLOUR(184, 134, 11) },
+    { "darkgray", RGB_TO_COLOUR(169, 169, 169) },
+    { "darkgreen", RGB_TO_COLOUR(0, 100, 0) },
+    { "darkgrey", RGB_TO_COLOUR(169, 169, 169) },
+    { "darkkhaki", RGB_TO_COLOUR(189, 183, 107) },
+    { "darkmagenta", RGB_TO_COLOUR(139, 0, 139) },
+    { "darkolivegreen", RGB_TO_COLOUR(85, 107, 47) },
+    { "darkorange", RGB_TO_COLOUR(255, 140, 0) },
+    { "darkorchid", RGB_TO_COLOUR(153, 50, 204) },
+    { "darkred", RGB_TO_COLOUR(139, 0, 0) },
+    { "darksalmon", RGB_TO_COLOUR(233, 150, 122) },
+    { "darkseagreen", RGB_TO_COLOUR(143, 188, 143) },
+    { "darkslateblue", RGB_TO_COLOUR(72, 61, 139) },
+    { "darkslategray", RGB_TO_COLOUR(47, 79, 79) },
+    { "darkslategrey", RGB_TO_COLOUR(47, 79, 79) },
+    { "darkturquoise", RGB_TO_COLOUR(0, 206, 209) },
+    { "darkviolet", RGB_TO_COLOUR(148, 0, 211) },
+    { "deeppink", RGB_TO_COLOUR(255, 20, 147) },
+    { "deepskyblue", RGB_TO_COLOUR(0, 191, 255) },
+    { "dimgray", RGB_TO_COLOUR(105, 105, 105) },
+    { "dimgrey", RGB_TO_COLOUR(105, 105, 105) },
+    { "dodgerblue", RGB_TO_COLOUR(30, 144, 255) },
+    { "firebrick", RGB_TO_COLOUR(178, 34, 34) },
+    { "floralwhite", RGB_TO_COLOUR(255, 250, 240) },
+    { "forestgreen", RGB_TO_COLOUR(34, 139, 34) },
+    { "fuchsia", RGB_TO_COLOUR(255, 0, 255) },
+    { "gainsboro", RGB_TO_COLOUR(220, 220, 220) },
+    { "ghostwhite", RGB_TO_COLOUR(248, 248, 255) },
+    { "gold", RGB_TO_COLOUR(255, 215, 0) },
+    { "goldenrod", RGB_TO_COLOUR(218, 165, 32) },
+    { "gray", RGB_TO_COLOUR(128, 128, 128) },
+    { "green", RGB_TO_COLOUR(0, 128, 0) },
+    { "greenyellow", RGB_TO_COLOUR(173, 255, 47) },
+    { "grey", RGB_TO_COLOUR(128, 128, 128) },
+    { "honeydew", RGB_TO_COLOUR(240, 255, 240) },
+    { "hotpink", RGB_TO_COLOUR(255, 105, 180) },
+    { "indianred", RGB_TO_COLOUR(205, 92, 92) },
+    { "indigo", RGB_TO_COLOUR(75, 0, 130) },
+    { "ivory", RGB_TO_COLOUR(255, 255, 240) },
+    { "khaki", RGB_TO_COLOUR(240, 230, 140) },
+    { "lavender", RGB_TO_COLOUR(230, 230, 250) },
+    { "lavenderblush", RGB_TO_COLOUR(255, 240, 245) },
+    { "lawngreen", RGB_TO_COLOUR(124, 252, 0) },
+    { "lemonchiffon", RGB_TO_COLOUR(255, 250, 205) },
+    { "lightblue", RGB_TO_COLOUR(173, 216, 230) },
+    { "lightcoral", RGB_TO_COLOUR(240, 128, 128) },
+    { "lightcyan", RGB_TO_COLOUR(224, 255, 255) },
+    { "lightgoldenrodyellow", RGB_TO_COLOUR(250, 250, 210) },
+    { "lightgray", RGB_TO_COLOUR(211, 211, 211) },
+    { "lightgreen", RGB_TO_COLOUR(144, 238, 144) },
+    { "lightgrey", RGB_TO_COLOUR(211, 211, 211) },
+    { "lightpink", RGB_TO_COLOUR(255, 182, 193) },
+    { "lightsalmon", RGB_TO_COLOUR(255, 160, 122) },
+    { "lightseagreen", RGB_TO_COLOUR(32, 178, 170) },
+    { "lightskyblue", RGB_TO_COLOUR(135, 206, 250) },
+    { "lightslategray", RGB_TO_COLOUR(119, 136, 153) },
+    { "lightslategrey", RGB_TO_COLOUR(119, 136, 153) },
+    { "lightsteelblue", RGB_TO_COLOUR(176, 196, 222) },
+    { "lightyellow", RGB_TO_COLOUR(255, 255, 224) },
+    { "lime", RGB_TO_COLOUR(0, 255, 0) },
+    { "limegreen", RGB_TO_COLOUR(50, 205, 50) },
+    { "linen", RGB_TO_COLOUR(250, 240, 230) },
+    { "magenta", RGB_TO_COLOUR(255, 0, 255) },
+    { "maroon", RGB_TO_COLOUR(128, 0, 0) },
+    { "mediumaquamarine", RGB_TO_COLOUR(102, 205, 170) },
+    { "mediumblue", RGB_TO_COLOUR(0, 0, 205) },
+    { "mediumorchid", RGB_TO_COLOUR(186, 85, 211) },
+    { "mediumpurple", RGB_TO_COLOUR(147, 112, 219) },
+    { "mediumseagreen", RGB_TO_COLOUR(60, 179, 113) },
+    { "mediumslateblue", RGB_TO_COLOUR(123, 104, 238) },
+    { "mediumspringgreen", RGB_TO_COLOUR(0, 250, 154) },
+    { "mediumturquoise", RGB_TO_COLOUR(72, 209, 204) },
+    { "mediumvioletred", RGB_TO_COLOUR(199, 21, 133) },
+    { "midnightblue", RGB_TO_COLOUR(25, 25, 112) },
+    { "mintcream", RGB_TO_COLOUR(245, 255, 250) },
+    { "mistyrose", RGB_TO_COLOUR(255, 228, 225) },
+    { "moccasin", RGB_TO_COLOUR(255, 228, 181) },
+    { "navajowhite", RGB_TO_COLOUR(255, 222, 173) },
+    { "navy", RGB_TO_COLOUR(0, 0, 128) },
+    { "oldlace", RGB_TO_COLOUR(253, 245, 230) },
+    { "olive", RGB_TO_COLOUR(128, 128, 0) },
+    { "olivedrab", RGB_TO_COLOUR(107, 142, 35) },
+    { "orange", RGB_TO_COLOUR(255, 165, 0) },
+    { "orangered", RGB_TO_COLOUR(255, 69, 0) },
+    { "orchid", RGB_TO_COLOUR(218, 112, 214) },
+    { "palegoldenrod", RGB_TO_COLOUR(238, 232, 170) },
+    { "palegreen", RGB_TO_COLOUR(152, 251, 152) },
+    { "paleturquoise", RGB_TO_COLOUR(175, 238, 238) },
+    { "palevioletred", RGB_TO_COLOUR(219, 112, 147) },
+    { "papayawhip", RGB_TO_COLOUR(255, 239, 213) },
+    { "peachpuff", RGB_TO_COLOUR(255, 218, 185) },
+    { "peru", RGB_TO_COLOUR(205, 133, 63) },
+    { "pink", RGB_TO_COLOUR(255, 192, 203) },
+    { "plum", RGB_TO_COLOUR(221, 160, 221) },
+    { "powderblue", RGB_TO_COLOUR(176, 224, 230) },
+    { "purple", RGB_TO_COLOUR(128, 0, 128) },
+    { "rebeccapurple", RGB_TO_COLOUR(102, 51, 153) },
+    { "red", RGB_TO_COLOUR(255, 0, 0) },
+    { "rosybrown", RGB_TO_COLOUR(188, 143, 143) },
+    { "royalblue", RGB_TO_COLOUR(65, 105, 225) },
+    { "saddlebrown", RGB_TO_COLOUR(139, 69, 19) },
+    { "salmon", RGB_TO_COLOUR(250, 128, 114) },
+    { "sandybrown", RGB_TO_COLOUR(244, 164, 96) },
+    { "seagreen", RGB_TO_COLOUR(46, 139, 87) },
+    { "seashell", RGB_TO_COLOUR(255, 245, 238) },
+    { "sienna", RGB_TO_COLOUR(160, 82, 45) },
+    { "silver", RGB_TO_COLOUR(192, 192, 192) },
+    { "skyblue", RGB_TO_COLOUR(135, 206, 235) },
+    { "slateblue", RGB_TO_COLOUR(106, 90, 205) },
+    { "slategray", RGB_TO_COLOUR(112, 128, 144) },
+    { "slategrey", RGB_TO_COLOUR(112, 128, 144) },
+    { "snow", RGB_TO_COLOUR(255, 250, 250) },
+    { "springgreen", RGB_TO_COLOUR(0, 255, 127) },
+    { "steelblue", RGB_TO_COLOUR(70, 130, 180) },
+    { "tan", RGB_TO_COLOUR(210, 180, 140) },
+    { "teal", RGB_TO_COLOUR(0, 128, 128) },
+    { "thistle", RGB_TO_COLOUR(216, 191, 216) },
+    { "tomato", RGB_TO_COLOUR(255, 99, 71) },
+    { "turquoise", RGB_TO_COLOUR(64, 224, 208) },
+    { "violet", RGB_TO_COLOUR(238, 130, 238) },
+    { "wheat", RGB_TO_COLOUR(245, 222, 179) },
+    { "white", RGB_TO_COLOUR(255, 255, 255) },
+    { "whitesmoke", RGB_TO_COLOUR(245, 245, 245) },
+    { "yellow", RGB_TO_COLOUR(255, 255, 0) },
+    { "yellowgreen", RGB_TO_COLOUR(154, 205, 50) },
+};
+
+#define IS_BETWEEN(x, low, high) ( ( (x) >= (low) ) && ( (x) <= (high) ) )
+
+static inline gboolean
+_nk_colour_parse_hex(gdouble *r, gchar c1, gchar c2)
+{
+    gchar s[3] = { c1, c2, '\0' }, *e;
+    guint64 v;
+
+    v = g_ascii_strtoull(s, &e, 16);
+    if ( s == e )
+        return FALSE;
+    if ( v > 255 )
+        return FALSE;
+
+    *r = (gdouble) v / 255.;
+    return TRUE;
+}
+
+static inline gboolean
+_nk_colour_parse_alpha_value(gdouble *r, gboolean expected)
+{
+    if ( expected != ( g_scanner_peek_next_token(_nk_colour_scanner) == G_TOKEN_COMMA ) )
+        return FALSE;
+    if ( ! expected )
+        return TRUE;
+    g_scanner_get_next_token(_nk_colour_scanner);
+
+    if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_FLOAT )
+        return FALSE;
+
+    gdouble v = _nk_colour_scanner->value.v_float;
+    if ( g_scanner_peek_next_token(_nk_colour_scanner) == '%' )
+    {
+        g_scanner_get_next_token(_nk_colour_scanner);
+
+        if ( ! IS_BETWEEN(v, 0., 100.) )
+            return FALSE;
+
+        *r = v / 100.;
+    }
+    else
+    {
+        if ( ! IS_BETWEEN(v, 0., 1.) )
+            return FALSE;
+
+        *r = v;
+    }
+
+    return TRUE;
+}
+
+static inline gboolean
+_nk_colour_parse_rgb_value(gdouble *r)
+{
+    if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_FLOAT )
+        return FALSE;
+
+    gdouble v = _nk_colour_scanner->value.v_float;
+    if ( g_scanner_peek_next_token(_nk_colour_scanner) == '%' )
+    {
+        g_scanner_get_next_token(_nk_colour_scanner);
+
+        if ( ! IS_BETWEEN(v, 0., 100.) )
+            return FALSE;
+
+        *r = v / 100.;
+    }
+    else
+    {
+        if ( ! IS_BETWEEN(v, 0., 255.) )
+            return FALSE;
+
+        *r = v / 255.;
+    }
+
+    return TRUE;
+}
+
+static inline gboolean
+_nk_colour_parse_hue_value(gdouble *r)
+{
+    if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_FLOAT )
+        return FALSE;
+
+    g_scanner_set_scope(_nk_colour_scanner, NK_COLOUR_SCOPE_ANGLES);
+
+    gdouble v = _nk_colour_scanner->value.v_float;
+    NkColourAngle unit = NK_COLOUR_ANGLE_DEG;
+    if ( g_scanner_peek_next_token(_nk_colour_scanner) == G_TOKEN_SYMBOL )
+    {
+        g_scanner_get_next_token(_nk_colour_scanner);
+        unit = _nk_colour_scanner->value.v_int;
+    }
+    if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_COMMA )
+        return FALSE;
+    g_scanner_set_scope(_nk_colour_scanner, NK_COLOUR_SCOPE_BASE);
+
+    switch ( unit )
+    {
+    case NK_COLOUR_ANGLE_DEG:
+        v /= 60.;
+    break;
+    case NK_COLOUR_ANGLE_GRAD:
+        v = 3. * v / 200.;
+    break;
+    case NK_COLOUR_ANGLE_RAD:
+        v = 3. * v / G_PI;
+    break;
+    case NK_COLOUR_ANGLE_TURN:
+        v *= 6.;
+    break;
+    }
+
+    *r = DMODULO(v, 6);
+
+    return TRUE;
+}
+
+static inline gboolean
+_nk_colour_parse_percent_value(gdouble *r)
+{
+    if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_FLOAT )
+        return FALSE;
+
+    gdouble v = _nk_colour_scanner->value.v_float;
+    if ( g_scanner_get_next_token(_nk_colour_scanner) != '%' )
+        return FALSE;
+    if ( ! IS_BETWEEN(v, 0., 100.) )
+        return FALSE;
+
+    *r = v / 100.;
+
+    return TRUE;
+}
+
+static void
+_nk_colour_hsl_to_rgb(NkColour *colour, gdouble h, gdouble s, gdouble l)
+{
+    gdouble c = ( 1. - ABS(2. * l - 1.) ) * s;
+    gdouble hh = DMODULO(h, 2);
+    gdouble x = c * ( 1. - ABS(hh - 1.) );
+    gdouble m = l - c / 2.;
+
+    gdouble r = 0., g = 0., b = 0.;
+    if ( IS_BETWEEN(h, 0., 1.) )
+    {
+        r = c;
+        g = x;
+    }
+    else if ( IS_BETWEEN(h, 1., 2.) )
+    {
+        r = x;
+        g = c;
+    }
+    else if ( IS_BETWEEN(h, 2., 3.) )
+    {
+        g = c;
+        b = x;
+    }
+    else if ( IS_BETWEEN(h, 3., 4.) )
+    {
+        g = x;
+        b = c;
+    }
+    else if ( IS_BETWEEN(h, 4., 5.) )
+    {
+        r = x;
+        b = c;
+    }
+    else if ( IS_BETWEEN(h, 5., 6.) )
+    {
+        r = c;
+        b = x;
+    }
+    colour->red   = r + m;
+    colour->green = g + m;
+    colour->blue  = b + m;
+}
+
+static void
+_nk_colour_hwb_to_rgb(NkColour *colour, gdouble h, gdouble w, gdouble b)
+{
+    _nk_colour_hsl_to_rgb(colour, h, 1., .5);
+    colour->red   *= ( 1. - w - b );
+    colour->red   += w;
+    colour->green *= ( 1. - w - b );
+    colour->green += w;
+    colour->blue  *= ( 1. - w - b );
+    colour->blue  += w;
+}
+
+static gboolean
+_nk_colour_search_named(NkColour *colour, const gchar *name)
+{
+    gsize i;
+    for ( i = 0 ; i < G_N_ELEMENTS(_nk_colour_scanner_named_colours) ; ++i )
+    {
+        if ( g_ascii_strcasecmp(name, _nk_colour_scanner_named_colours[i].name) == 0 )
+        {
+            *colour = _nk_colour_scanner_named_colours[i].value;
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+gboolean
+nk_colour_parse(const gchar *s, NkColour *colour)
+{
+    if ( _nk_colour_scanner == NULL )
+    {
+        _nk_colour_scanner = g_scanner_new(NULL);
+        _nk_colour_scanner->config->int_2_float = TRUE;
+        _nk_colour_scanner->config->cset_identifier_first = "#" G_CSET_a_2_z G_CSET_A_2_Z G_CSET_LATINS G_CSET_LATINC;
+        _nk_colour_scanner->config->cpair_comment_single = "";
+        _nk_colour_scanner->config->case_sensitive = TRUE;
+
+        gsize i;
+        for ( i = 0 ; i < G_N_ELEMENTS(_nk_colour_scanner_symbols_base) ; ++i )
+            g_scanner_scope_add_symbol(_nk_colour_scanner, NK_COLOUR_SCOPE_BASE, _nk_colour_scanner_symbols_base[i], GUINT_TO_POINTER(i));
+        for ( i = 0 ; i < G_N_ELEMENTS(_nk_colour_scanner_symbols_angle_units) ; ++i )
+            g_scanner_scope_add_symbol(_nk_colour_scanner, NK_COLOUR_SCOPE_ANGLES, _nk_colour_scanner_symbols_angle_units[i], GUINT_TO_POINTER(i));
+    }
+
+    if ( s == NULL )
+        return FALSE;
+
+    g_scanner_input_text(_nk_colour_scanner, s, strlen(s));
+    g_scanner_set_scope(_nk_colour_scanner, NK_COLOUR_SCOPE_BASE);
+
+    NkColour colour_ = { .alpha = 1. };
+    switch ( g_scanner_get_next_token(_nk_colour_scanner) )
+    {
+    case G_TOKEN_SYMBOL:
+    {
+        NkColourSymbolBase symbol = _nk_colour_scanner->value.v_int;
+        gboolean alpha = FALSE;
+        switch ( symbol )
+        {
+        case NK_COLOUR_SYMBOL_BASE_RGBA:
+            alpha = TRUE;
+            /* fallthrough */
+        case NK_COLOUR_SYMBOL_BASE_RGB:
+            if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_LEFT_PAREN )
+                return FALSE;
+            if ( ! _nk_colour_parse_rgb_value(&colour_.red) )
+                return FALSE;
+            if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_COMMA )
+                return FALSE;
+            if ( ! _nk_colour_parse_rgb_value(&colour_.green) )
+                return FALSE;
+            if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_COMMA )
+                return FALSE;
+            if ( ! _nk_colour_parse_rgb_value(&colour_.blue) )
+                return FALSE;
+            if ( ! _nk_colour_parse_alpha_value(&colour_.alpha, alpha) )
+                return FALSE;
+            if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_RIGHT_PAREN )
+                return FALSE;
+        break;
+        case NK_COLOUR_SYMBOL_BASE_HSLA:
+            alpha = TRUE;
+            /* fallthrough */
+        case NK_COLOUR_SYMBOL_BASE_HSL:
+        {
+            gdouble h, s, l;
+            if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_LEFT_PAREN )
+                return FALSE;
+            if ( ! _nk_colour_parse_hue_value(&h) )
+                return FALSE;
+            if ( ! _nk_colour_parse_percent_value(&s) )
+                return FALSE;
+            if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_COMMA )
+                return FALSE;
+            if ( ! _nk_colour_parse_percent_value(&l) )
+                return FALSE;
+            if ( ! _nk_colour_parse_alpha_value(&colour_.alpha, alpha) )
+                return FALSE;
+            if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_RIGHT_PAREN )
+                return FALSE;
+
+            _nk_colour_hsl_to_rgb(&colour_, h, s, l);
+        }
+        break;
+        case NK_COLOUR_SYMBOL_BASE_HWB:
+        {
+            gdouble h, w, b;
+            if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_LEFT_PAREN )
+                return FALSE;
+            if ( ! _nk_colour_parse_hue_value(&h) )
+                return FALSE;
+            if ( ! _nk_colour_parse_percent_value(&w) )
+                return FALSE;
+            if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_COMMA )
+                return FALSE;
+            if ( ! _nk_colour_parse_percent_value(&b) )
+                return FALSE;
+            if ( ! _nk_colour_parse_alpha_value(&colour_.alpha, g_scanner_peek_next_token(_nk_colour_scanner) == G_TOKEN_COMMA) )
+                return FALSE;
+            if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_RIGHT_PAREN )
+                return FALSE;
+
+            _nk_colour_hwb_to_rgb(&colour_, h, w, b);
+        }
+        break;
+        }
+    }
+    break;
+    case G_TOKEN_IDENTIFIER:
+        if ( _nk_colour_scanner->value.v_identifier[0] == '#' )
+        {
+            if ( g_scanner_peek_next_token(_nk_colour_scanner) != G_TOKEN_EOF )
+                return FALSE;
+
+            const gchar *hex = _nk_colour_scanner->value.v_identifier + strlen("#");
+            switch ( strlen(hex) )
+            {
+            case 8: /* rrggbbaa */
+                if ( ! _nk_colour_parse_hex(&colour_.alpha, hex[6], hex[7]) )
+                    return FALSE;
+                /* fallthrough */
+            case 6: /* rrggbb */
+                if ( ! _nk_colour_parse_hex(&colour_.red, hex[0], hex[1]) )
+                    return FALSE;
+                if ( ! _nk_colour_parse_hex(&colour_.green, hex[2], hex[3]) )
+                    return FALSE;
+                if ( ! _nk_colour_parse_hex(&colour_.blue, hex[4], hex[5]) )
+                    return FALSE;
+            break;
+            case 4: /* rgba */
+                if ( ! _nk_colour_parse_hex(&colour_.alpha, hex[3], hex[3]) )
+                    return FALSE;
+                /* fallthrough */
+            case 3: /* rgb */
+                if ( ! _nk_colour_parse_hex(&colour_.red, hex[0], hex[0]) )
+                    return FALSE;
+                if ( ! _nk_colour_parse_hex(&colour_.green, hex[1], hex[1]) )
+                    return FALSE;
+                if ( ! _nk_colour_parse_hex(&colour_.blue, hex[2], hex[2]) )
+                    return FALSE;
+            break;
+            default:
+                return FALSE;
+            }
+        }
+        else if ( ! _nk_colour_search_named(&colour_, _nk_colour_scanner->value.v_identifier) )
+            return FALSE;
+    break;
+    case G_TOKEN_STRING:
+        if ( ! _nk_colour_search_named(&colour_, _nk_colour_scanner->value.v_string) )
+            return FALSE;
+        /* fallthrough */
+    default:
+        return FALSE;
+    }
+
+    if ( g_scanner_get_next_token(_nk_colour_scanner) != G_TOKEN_EOF )
+        return FALSE;
+
+    *colour = colour_;
+
+    return TRUE;
+}
+
+#define HEX_COLOUR_MAXLEN 10 /* strlen("#rrggbbaa") + 1 */
+const gchar *
+nk_colour_to_hex(const NkColour *colour)
+{
+    guint8 red   = (guint8) ( colour->red   * 255. + 0.5 );
+    guint8 green = (guint8) ( colour->green * 255. + 0.5 );
+    guint8 blue  = (guint8) ( colour->blue  * 255. + 0.5 );
+    guint8 alpha = (guint8) ( colour->alpha * 255. + 0.5 );
+
+    static gchar string[HEX_COLOUR_MAXLEN];
+    if ( alpha != 0xff )
+        g_snprintf(string, HEX_COLOUR_MAXLEN, "#%02x%02x%02x%02x", red, green, blue, alpha);
+    else
+        g_snprintf(string, HEX_COLOUR_MAXLEN, "#%02x%02x%02x", red, green, blue);
+    return string;
+}
+
+#define COLOUR_DOUBLE_RGBA_MAXLEN 64 /* strlen("rgba(255.0000000000,255.0000000000,255.0000000000,0.0000000000)") + 1 */
+const gchar *
+nk_colour_to_rgba(const NkColour *colour)
+{
+    gdouble red   = colour->red   * 255.;
+    gdouble green = colour->green * 255.;
+    gdouble blue  = colour->blue  * 255.;
+    gdouble alpha = colour->alpha;
+
+    static gchar string[COLOUR_DOUBLE_RGBA_MAXLEN];
+    if ( alpha != 1.0 )
+        g_snprintf(string, COLOUR_DOUBLE_RGBA_MAXLEN, "rgba(%.10lf,%.10lf,%.10lf,%.10lf)", red, green, blue, alpha);
+    else
+        g_snprintf(string, COLOUR_DOUBLE_RGBA_MAXLEN, "rgb(%.10lf,%.10lf,%.10lf)", red, green, blue);
+    return string;
+}
+
+#define COLOUR_DOUBLE_RGBA_MAXLEN 64 /* strlen("rgba(255.0000000000,255.0000000000,255.0000000000,0.0000000000)") + 1 */
+const gchar *
+nk_colour_to_hsla(const NkColour *colour)
+{
+    gdouble red   = colour->red   * 255.;
+    gdouble green = colour->green * 255.;
+    gdouble blue  = colour->blue  * 255.;
+    gdouble alpha = colour->alpha;
+
+    static gchar string[COLOUR_DOUBLE_RGBA_MAXLEN];
+    if ( alpha != 1.0 )
+        g_snprintf(string, COLOUR_DOUBLE_RGBA_MAXLEN, "rgba(%.10lf,%.10lf,%.10lf,%.10lf)", red, green, blue, alpha);
+    else
+        g_snprintf(string, COLOUR_DOUBLE_RGBA_MAXLEN, "rgb(%.10lf,%.10lf,%.10lf)", red, green, blue);
+    return string;
+}
+
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/enum.c
@@ -0,0 +1,80 @@
+/*
+ * libnkutils/enum - Miscellaneous utilities, enum module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "libnkutils-enum"
+
+#include <string.h>
+
+#include <glib.h>
+
+#include "nkutils-enum.h"
+
+static inline gint
+nk_str_equal(gboolean ignore_case, gboolean prefix, const gchar *token, const gchar *string)
+{
+    if ( ( token == NULL ) || ( string == NULL ) || ( token == string ) )
+        return ( token == string );
+
+    gunichar wt = g_utf8_get_char(token), ws = g_utf8_get_char(string);
+    while ( wt != '\0' )
+    {
+        if ( ignore_case )
+        {
+            wt = g_unichar_tolower(wt);
+            ws = g_unichar_tolower(ws);
+        }
+
+        if ( wt != ws )
+            return FALSE;
+
+        token = g_utf8_next_char(token);
+        string = g_utf8_next_char(string);
+        wt = g_utf8_get_char(token);
+        ws = g_utf8_get_char(string);
+    }
+    return ( prefix || ( ws == '\0' ) );
+}
+
+gboolean
+nk_enum_parse(const gchar *string, const gchar * const *values, guint64 size, gboolean ignore_case, gboolean prefix, guint64 *value)
+{
+    guint64 i;
+    for ( i = 0 ; i < size ; ++i )
+    {
+        if ( nk_str_equal(ignore_case, prefix, values[i], string) )
+        {
+            *value = i;
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/gtk-settings.c
@@ -0,0 +1,157 @@
+/*
+ * libnkutils/xdg-de - Miscellaneous utilities, XDG DE module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "libnkutils-gtk-settings"
+
+#include <glib.h>
+
+#include "nkutils-gtk-settings.h"
+
+typedef struct {
+    const gchar *path;
+    const gchar *group;
+} NkGtkSettingsFile;
+
+static const NkGtkSettingsFile _nk_gtk_settings_files[NK_GTK_SETTINGS_NUM_VERSION] = {
+        {
+            .path = "gtk-3.0" G_DIR_SEPARATOR_S "settings.ini",
+            .group = "Settings",
+        },
+        {
+            .path = "gtk-4.0" G_DIR_SEPARATOR_S "settings.ini",
+            .group = "Settings",
+        },
+};
+
+typedef gboolean (*NkGtkSettingsGetter)(gpointer value, GKeyFile *settings, const gchar *group, const gchar *key);
+static gboolean
+_nk_gtk_settings_try_dir(gpointer value, const gchar *keys[NK_GTK_SETTINGS_NUM_VERSION], NkGtkSettingsGetter getter, const gchar *dir)
+{
+    gboolean found = FALSE;
+    gssize i;
+    for ( i = NK_GTK_SETTINGS_NUM_VERSION - 1 ; ( ! found ) && ( i >= 0 ) ; --i )
+    {
+        const NkGtkSettingsFile *file = &_nk_gtk_settings_files[i];
+        gchar *path;
+        GKeyFile *settings;
+
+        path = g_build_filename(dir, file->path, NULL);
+        settings = g_key_file_new();
+
+        if ( g_key_file_load_from_file(settings, path, G_KEY_FILE_NONE, NULL)
+             && g_key_file_has_group(settings, file->group)
+             && g_key_file_has_key(settings, file->group, keys[i], NULL) )
+            found = getter(value, settings, file->group, keys[i]);
+        g_key_file_free(settings);
+        g_free(path);
+    }
+
+    return found;
+}
+
+gboolean
+_nk_gtk_settings_get(gpointer value, const gchar *keys[NK_GTK_SETTINGS_NUM_VERSION], NkGtkSettingsGetter getter)
+{
+    if ( _nk_gtk_settings_try_dir(value, keys, getter, g_get_user_config_dir()) )
+        return TRUE;
+    const gchar * const *dir;
+    dir = g_get_system_config_dirs();
+    for ( ; *dir != NULL ; ++dir )
+    {
+        if ( _nk_gtk_settings_try_dir(value, keys, getter, *dir) )
+            return TRUE;
+    }
+    return _nk_gtk_settings_try_dir(value, keys, getter, SYSCONFDIR);
+}
+
+static gboolean
+_nk_gtk_settings_getter_boolean(gpointer ret_, GKeyFile *settings, const gchar *group, const gchar *key)
+{
+    gboolean *ret = ret_, value;
+    GError *error = NULL;
+
+    value = g_key_file_get_boolean(settings, group, key, &error);
+    if ( error == NULL )
+        *ret = value;
+    else
+        g_error_free(error);
+
+    return ( error == NULL );
+}
+
+gboolean
+nk_gtk_settings_get_boolean(gboolean *value, const gchar *keys[NK_GTK_SETTINGS_NUM_VERSION])
+{
+    return _nk_gtk_settings_get(value, keys, _nk_gtk_settings_getter_boolean);
+}
+
+static gboolean
+_nk_gtk_settings_getter_uint64(gpointer ret_, GKeyFile *settings, const gchar *group, const gchar *key)
+{
+    guint64 *ret = ret_, value;
+    GError *error = NULL;
+
+    value = g_key_file_get_uint64(settings, group, key, &error);
+    if ( error == NULL )
+        *ret = value;
+    else
+        g_error_free(error);
+
+    return ( error == NULL );
+}
+
+gboolean
+nk_gtk_settings_get_uint64(guint64 *value, const gchar *keys[NK_GTK_SETTINGS_NUM_VERSION])
+{
+    return _nk_gtk_settings_get(value, keys, _nk_gtk_settings_getter_uint64);
+}
+
+static gboolean
+_nk_gtk_settings_getter_string(gpointer ret_, GKeyFile *settings, const gchar *group, const gchar *key)
+{
+    gchar **ret = ret_, *value;
+    GError *error = NULL;
+
+    value = g_key_file_get_string(settings, group, key, &error);
+    if ( error == NULL )
+        *ret = value;
+    else
+        g_error_free(error);
+
+    return ( error == NULL );
+}
+
+gboolean
+nk_gtk_settings_get_string(gchar **value, const gchar *keys[NK_GTK_SETTINGS_NUM_VERSION])
+{
+    return _nk_gtk_settings_get(value, keys, _nk_gtk_settings_getter_string);
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/nkutils-bindings.h
@@ -0,0 +1,100 @@
+/*
+ * libnkutils/bindings - Miscellaneous utilities, bindings module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef __NK_UTILS_BINDINGS_H__
+#define __NK_UTILS_BINDINGS_H__
+
+#include <xkbcommon/xkbcommon.h>
+
+typedef struct _NkBindings NkBindings;
+typedef struct _NkBindingsSeat NkBindingsSeat;
+
+typedef enum {
+    NK_BINDINGS_MODIFIER_SHIFT,
+    NK_BINDINGS_MODIFIER_CONTROL,
+    NK_BINDINGS_MODIFIER_ALT,
+    NK_BINDINGS_MODIFIER_SUPER,
+    NK_BINDINGS_MODIFIER_META,
+    NK_BINDINGS_MODIFIER_HYPER,
+#define NK_BINDINGS_NUM_MODIFIERS 6
+} NkBindingsModifiers;
+
+typedef enum {
+    NK_BINDINGS_KEY_STATE_PRESS,
+    NK_BINDINGS_KEY_STATE_PRESSED,
+    NK_BINDINGS_KEY_STATE_RELEASE,
+} NkBindingsKeyState;
+
+typedef enum {
+    NK_BINDINGS_BUTTON_STATE_PRESS,
+    NK_BINDINGS_BUTTON_STATE_RELEASE,
+} NkBindingsButtonState;
+
+#define NK_BINDINGS_MODIFIER_MASK(m) (1 << (m))
+
+typedef enum {
+    NK_BINDINGS_MOUSE_BUTTON_PRIMARY,
+    NK_BINDINGS_MOUSE_BUTTON_SECONDARY,
+    NK_BINDINGS_MOUSE_BUTTON_MIDDLE,
+    NK_BINDINGS_MOUSE_BUTTON_BACK,
+    NK_BINDINGS_MOUSE_BUTTON_FORWARD,
+    NK_BINDINGS_MOUSE_BUTTON_EXTRA,
+} NkBindingsMouseButton;
+
+typedef enum {
+    NK_BINDINGS_SCROLL_AXIS_VERTICAL,
+    NK_BINDINGS_SCROLL_AXIS_HORIZONTAL,
+#define NK_BINDINGS_SCROLL_NUM_AXIS 2
+} NkBindingsScrollAxis;
+
+typedef enum {
+    NK_BINDINGS_ERROR_SYNTAX,
+    NK_BINDINGS_ERROR_NOTHING,
+    NK_BINDINGS_ERROR_ALREADY_REGISTERED,
+} NkBindingsError;
+#define NK_BINDINGS_ERROR (nk_bindings_error())
+GQuark nk_bindings_error(void);
+
+NkBindings *nk_bindings_new(guint64 double_click_delay);
+void nk_bindings_free(NkBindings *bindings);
+
+typedef gboolean (*NkBindingsCallback)(guint64 scope, gpointer target, gpointer user_data);
+gboolean nk_bindings_add_binding(NkBindings *bindings, guint64 scope, const gchar *string, NkBindingsCallback callback, gpointer user_data, GDestroyNotify notify, GError **error);
+void nk_bindings_reset_bindings(NkBindings *bindings);
+
+NkBindingsSeat *nk_bindings_seat_new(NkBindings *bindings, enum xkb_context_flags flags);
+void nk_bindings_seat_free(NkBindingsSeat *seat);
+void nk_bindings_seat_update_keymap(NkBindingsSeat *seat, struct xkb_keymap *keymap, struct xkb_state *state);
+
+struct xkb_context *nk_bindings_seat_get_context(NkBindingsSeat *seat);
+
+gchar *nk_bindings_seat_handle_key(NkBindingsSeat *seat, gpointer target, xkb_keycode_t key, NkBindingsKeyState state);
+gchar *nk_bindings_seat_handle_key_with_modmask(NkBindingsSeat *self, gpointer target, xkb_mod_mask_t modmask, xkb_keycode_t keycode, NkBindingsKeyState state);
+gboolean nk_bindings_seat_handle_button(NkBindingsSeat *seat, gpointer target, NkBindingsMouseButton button, NkBindingsButtonState state, guint64 timestamp);
+gboolean nk_bindings_seat_handle_scroll(NkBindingsSeat *seat, gpointer target, NkBindingsScrollAxis axis, gint32 steps);
+void nk_bindings_seat_update_mask(NkBindingsSeat *seat, gpointer target, xkb_mod_mask_t depressed_mods, xkb_mod_mask_t latched_mods, xkb_mod_mask_t locked_mods, xkb_layout_index_t depressed_layout, xkb_layout_index_t latched_layout, xkb_layout_index_t locked_layout);
+void nk_bindings_seat_reset(NkBindingsSeat *seat);
+
+#endif /* __NK_UTILS_BINDINGS_H__ */
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/nkutils-colour.h
@@ -0,0 +1,41 @@
+/*
+ * libnkutils/colour - Miscellaneous utilities, colour module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef __NK_UTILS_COLOUR_H__
+#define __NK_UTILS_COLOUR_H__
+
+typedef struct {
+    gdouble red;
+    gdouble green;
+    gdouble blue;
+    gdouble alpha;
+} NkColour;
+
+gboolean nk_colour_parse(const gchar *string, NkColour *colour);
+const gchar *nk_colour_to_hex(const NkColour *colour);
+const gchar *nk_colour_to_rgba(const NkColour *colour);
+const gchar *nk_colour_to_hsl(const NkColour *colour);
+
+#endif /* __NK_UTILS_COLOUR_H__ */
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/nkutils-enum.h
@@ -0,0 +1,31 @@
+/*
+ * libnkutils/enum - Miscellaneous utilities, enum module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef __NK_UTILS_ENUM_H__
+#define __NK_UTILS_ENUM_H__
+
+gboolean nk_enum_parse(const gchar *string, const gchar * const *values, guint64 size, gboolean ignore_case, gboolean prefix, guint64 *value);
+
+#endif /* __NK_UTILS_ENUM_H__ */
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/nkutils-gtk-settings.h
@@ -0,0 +1,39 @@
+/*
+ * libnkutils/xdg-de - Miscellaneous utilities, GTK+ settings module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef __NK_UTILS_GTK_SETTINGS_H__
+#define __NK_UTILS_GTK_SETTINGS_H__
+
+typedef enum {
+    NK_GTK_SETTINGS_VERSION_3,
+    NK_GTK_SETTINGS_VERSION_4,
+    NK_GTK_SETTINGS_NUM_VERSION,
+} NkGtkSettingsVersion;
+
+gboolean nk_gtk_settings_get_boolean(gboolean *value, const gchar *keys[NK_GTK_SETTINGS_NUM_VERSION]);
+gboolean nk_gtk_settings_get_uint64(guint64 *value, const gchar *keys[NK_GTK_SETTINGS_NUM_VERSION]);
+gboolean nk_gtk_settings_get_string(gchar **value, const gchar *keys[NK_GTK_SETTINGS_NUM_VERSION]);
+
+#endif /* __NK_UTILS_GTK_SETTINGS_H__ */
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/nkutils-token.h
@@ -0,0 +1,50 @@
+/*
+ * libnkutils/token - Miscellaneous utilities, token module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef __NK_UTILS_TOKEN_H__
+#define __NK_UTILS_TOKEN_H__
+
+
+typedef enum {
+    NK_TOKEN_ERROR_WRONG_KEY,
+    NK_TOKEN_ERROR_UNKNOWN_MODIFIER,
+    NK_TOKEN_ERROR_REGEX,
+    NK_TOKEN_ERROR_UNKNOWN_TOKEN,
+} NkTokenError;
+
+typedef struct _NkTokenList NkTokenList;
+
+typedef const gchar *(*NkTokenListReplaceCallback)(const gchar *token, guint64 value, const gchar *key, gint64 index, gpointer user_data);
+
+GQuark nk_token_error_quark(void);
+#define NK_TOKEN_ERROR (nk_token_error_quark())
+
+NkTokenList *nk_token_list_parse(gchar *string, gunichar identifier, GError **error);
+NkTokenList *nk_token_list_parse_enum(gchar *string, gunichar identifier, const gchar * const *tokens, guint64 size, guint64 *used_tokens, GError **error);
+NkTokenList *nk_token_list_ref(NkTokenList *token_list);
+void nk_token_list_unref(NkTokenList *token_list);
+gchar *nk_token_list_replace(const NkTokenList *token_list, NkTokenListReplaceCallback callback, gpointer user_data);
+
+#endif /* __NK_UTILS_TOKEN_H__ */
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/nkutils-uuid.h
@@ -0,0 +1,46 @@
+/*
+ * libnkutils/uuid - Miscellaneous utilities, uuid module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef __NK_UTILS_UUID_H__
+#define __NK_UTILS_UUID_H__
+
+#define NK_UUID_LENGTH 16
+#define NK_UUID_FORMATTED_LENGTH 36
+
+typedef struct {
+    guchar data[NK_UUID_LENGTH];
+    gchar string[NK_UUID_FORMATTED_LENGTH + 1];
+} NkUuid;
+
+#define NK_UUID_INIT { .data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, .string = "00000000-0000-0000-0000-000000000000" }
+
+void nk_uuid_generate(NkUuid *uuid);
+gboolean nk_uuid_parse(NkUuid *uuid, const gchar *string);
+
+/* Uses SHA-1 checksum */
+/* uuid must contain the namespace UUID */
+void nk_uuid_from_name(NkUuid *uuid, const gchar *name, gssize length);
+
+#endif /* __NK_UTILS_UUID_H__ */
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/nkutils-xdg-de.h
@@ -0,0 +1,38 @@
+/*
+ * libnkutils/xdg-de - Miscellaneous utilities, XDG DE module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef __NK_UTILS_XDG_DE_H__
+#define __NK_UTILS_XDG_DE_H__
+
+typedef enum {
+    NK_XDG_DE_NONE = 0,
+    NK_XDG_DE_GNOME,
+    NK_XDG_DE_KDE,
+    NK_XDG_DE_I3,
+} NkXdgDe;
+
+NkXdgDe nk_xdg_de_detect(void);
+
+#endif /* __NK_UTILS_XDG_DE_H__ */
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/nkutils-xdg-theme.h
@@ -0,0 +1,40 @@
+/*
+ * libnkutils/xdg-theme - Miscellaneous utilities, XDG Theme module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef __NK_UTILS_XDG_THEME_H__
+#define __NK_UTILS_XDG_THEME_H__
+
+typedef struct _NkXdgThemeContext NkXdgThemeContext;
+
+NkXdgThemeContext *nk_xdg_theme_context_new(const gchar * const *icon_fallback_themes, const gchar * const *sound_fallback_themes);
+void nk_xdg_theme_context_free(NkXdgThemeContext *context);
+
+void nk_xdg_theme_preload_themes_icon(NkXdgThemeContext *context, const gchar * const *theme_names);
+void nk_xdg_theme_preload_themes_sound(NkXdgThemeContext *context, const gchar * const *theme_names);
+
+gchar *nk_xdg_theme_get_icon(NkXdgThemeContext *context, const gchar * const *themes, const gchar *context_name, const gchar *name, gint size, gint scale, gboolean svg);
+gchar *nk_xdg_theme_get_sound(NkXdgThemeContext *context, const gchar * const *themes, const gchar *name, const gchar *profile, const gchar *locale);
+
+#endif /* __NK_UTILS_XDG_THEME_H__ */
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/token.c
@@ -0,0 +1,502 @@
+/*
+ * libnkutils/token - Miscellaneous utilities, token module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "libnkutils-token"
+
+#include <string.h>
+#include <errno.h>
+
+#include <glib.h>
+
+#include "nkutils-enum.h"
+
+#include "nkutils-token.h"
+
+typedef struct {
+    GRegex *regex;
+    NkTokenList *replacement;
+} NkTokenRegex;
+
+typedef struct {
+    const gchar *string;
+    const gchar *name;
+    const gchar *key;
+    gint64 index;
+    guint64 value;
+    NkTokenList *fallback;
+    NkTokenList *substitute;
+    NkTokenRegex *replace;
+    gboolean no_data;
+} NkToken;
+
+struct _NkTokenList {
+    guint64 ref_count;
+    gchar *string;
+    gsize length;
+    NkToken *tokens;
+    gsize size;
+};
+
+
+GQuark
+nk_token_error_quark(void)
+{
+    return g_quark_from_static_string("nk_token_error-quark");
+}
+
+static gchar *
+_nk_token_strchr_escape(gchar *s, gsize l, gunichar c, gunichar pair_c)
+{
+    gchar *e = s + l;
+
+    gsize pair_count = 0;
+    gchar *w = s;
+    gunichar wc = '\0', pc;
+
+    for ( ; w < e ; w = g_utf8_next_char(w) )
+    {
+        pc = wc;
+        wc = g_utf8_get_char(w);
+
+        if ( pc == '\\' )
+        {
+            /* Escaped, search for next one */
+            if ( wc == '\\' )
+                /* Escaping a backslash, avoid escaping the next char */
+                wc = '\0';
+            else if ( wc == c )
+            {
+                w = g_utf8_prev_char(w);
+                pc = wc;
+                gchar *to, *from;
+                for ( to = w, from = g_utf8_next_char(w) ; from <= e ; ++to, ++from )
+                    *to = *from;
+                e = to;
+            }
+            continue;
+        }
+
+        /* Maybe do we open a paired character */
+        if ( ( pair_c != '\0' ) && ( wc == pair_c ) )
+            ++pair_count;
+
+        if ( wc != c )
+            continue;
+
+        /* We found our character, check if it is the right occurence */
+
+        if ( ( pair_c != '\0' ) && ( pair_count > 0 ) )
+        {
+            /* We had an opened pair, close it */
+            --pair_count;
+            continue;
+        }
+
+        return w;
+    }
+    return NULL;
+}
+
+NkTokenList *
+nk_token_list_parse(gchar *string, gunichar identifier, GError **error)
+{
+    g_return_val_if_fail(string != NULL, NULL);
+    g_return_val_if_fail(error == NULL || *error == NULL, NULL);
+
+    NkTokenList *self;
+
+    self = g_new0(NkTokenList, 1);
+    self->ref_count = 1;
+    self->string = string;
+    self->length = strlen(self->string);
+
+    gboolean have_identifier = ( identifier != '\0' );
+    if ( ! have_identifier )
+        identifier = '{';
+
+    gchar *w = string;
+    while ( ( w = g_utf8_strchr(w, self->length - ( w - self->string ), identifier) ) != NULL )
+    {
+        gchar *b = w;
+
+        if ( have_identifier )
+        {
+            w = g_utf8_next_char(w);
+            if ( g_utf8_get_char(w) == identifier )
+            {
+                *w = '\0';
+                if ( *string != '\0' )
+                {
+                    NkToken token = {
+                        .string = string
+                    };
+                    ++self->size;
+                    self->tokens = g_renew(NkToken, self->tokens, self->size);
+                    self->tokens[self->size - 1] = token;
+                }
+                string = w = g_utf8_next_char(w);
+                continue;
+            }
+
+            if ( g_utf8_get_char(w) != '{' )
+                continue;
+        }
+
+        w = g_utf8_next_char(w);
+        gchar *e;
+        NkToken token = {
+            .name = w
+        };
+
+        /* References are alpha/-/_ only */
+        while ( g_unichar_isalpha(g_utf8_get_char(w)) || ( g_utf8_get_char(w) == '-' ) || ( g_utf8_get_char(w) == '_' ) )
+            w = g_utf8_next_char(w);
+
+
+        e = _nk_token_strchr_escape(w, self->length - ( w - self->string ), '}', '{');
+        if ( e == NULL )
+            continue;
+        gchar *next = g_utf8_next_char(e);
+
+        if ( ( w != e ) && ( g_utf8_get_char(w) == '[' ) )
+        {
+            gchar *ss = w;
+            w = g_utf8_next_char(w);
+            const gchar *key = w;
+            gint64 index = 0;
+            if ( g_unichar_isdigit(g_utf8_get_char(w)) )
+            {
+                gchar *ie;
+                errno = 0;
+                key = "";
+                index = g_ascii_strtoll(w, &ie, 10);
+                if ( ( errno != 0 ) || ( w == ie ) )
+                {
+                    g_set_error(error, NK_TOKEN_ERROR, NK_TOKEN_ERROR_WRONG_KEY, "Could not parse index value: %s", ss);
+                    goto fail;
+                }
+                w = ie;
+            }
+            else if ( g_unichar_isalpha(g_utf8_get_char(w)) )
+            {
+                while ( g_unichar_isalpha(g_utf8_get_char(w)) || ( g_utf8_get_char(w) == '-' ) || ( g_utf8_get_char(w) == '_' ) )
+                    w = g_utf8_next_char(w);
+            }
+            else if ( g_utf8_get_char(w) == '@' )
+            {
+                /* We consider the rest as a join token */
+                while ( g_utf8_get_char(w) != ']' )
+                    w = g_utf8_next_char(w);
+            }
+            else
+                w = ss;
+
+            if ( g_utf8_get_char(w) != ']' )
+            {
+                g_set_error(error, NK_TOKEN_ERROR, NK_TOKEN_ERROR_WRONG_KEY, "Wrong key value: %s", ss);
+                goto fail;
+            }
+
+            gchar *ie = w;
+            w = g_utf8_next_char(w);
+            *ss = '\0';
+            *ie = '\0';
+            token.key = key;
+            token.index = index;
+        }
+
+        if ( w != e )
+        switch ( g_utf8_get_char(w) )
+        {
+        case ':':
+        {
+            gchar *m = w;
+            w = g_utf8_next_char(w);
+            *e = '\0';
+
+            switch ( g_utf8_get_char(w) )
+            {
+            case '-':
+                token.fallback = nk_token_list_parse(g_utf8_next_char(w), identifier, error);
+                if ( token.fallback == NULL )
+                    goto fail;
+            break;
+            case '+':
+                token.substitute = nk_token_list_parse(g_utf8_next_char(w), identifier, error);
+                if ( token.substitute == NULL )
+                    goto fail;
+            break;
+            case '!':
+                token.no_data = TRUE;
+                token.fallback = nk_token_list_parse(g_utf8_next_char(w), identifier, error);
+                if ( token.fallback == NULL )
+                    goto fail;
+            break;
+            default:
+                /* Just fail on malformed string */
+                *g_utf8_next_char(w) = '\0';
+                g_set_error(error, NK_TOKEN_ERROR, NK_TOKEN_ERROR_UNKNOWN_MODIFIER, "Wrong modifier value: %s", w);
+                goto fail;
+            }
+            *m = '\0';
+        }
+        break;
+        case '/':
+        {
+            gchar *m = w;
+            *e = '\0';
+
+            gsize c = 0;
+            do
+            {
+                ++c;
+                *m = '\0';
+            } while ( ( m = _nk_token_strchr_escape(m, e - m, '/', '\0') ) != NULL );
+            c = ( c + 1 ) / 2 + 1;
+
+            token.replace = g_new(NkTokenRegex, c);
+            c = 0;
+            do
+            {
+                GError *_inner_error_ = NULL;
+                token.replace[c].regex = g_regex_new(++w, G_REGEX_OPTIMIZE, 0, &_inner_error_);
+                if ( token.replace[c].regex == NULL )
+                {
+                    g_set_error(error, NK_TOKEN_ERROR, NK_TOKEN_ERROR_REGEX, "Wrong regex: %s", _inner_error_->message);
+                    g_clear_error(&_inner_error_);
+                    goto fail;
+                }
+
+                w = w + strlen(w) + 1;
+                token.replace[c].replacement = nk_token_list_parse(( w > e ) ? "" : w, identifier, error);
+                if ( token.replace[c].replacement == NULL )
+                    goto fail;
+                w += strlen(w);
+                ++c;
+            } while ( w < e );
+            token.replace[c].regex = NULL;
+        }
+        break;
+        default:
+            continue;
+        }
+
+        *e = *b = '\0';
+
+        ++self->size;
+        if ( *string != '\0' )
+            ++self->size;
+        self->tokens = g_renew(NkToken, self->tokens, self->size);
+        if ( *string != '\0' )
+        {
+            NkToken stoken = {
+                .string = string
+            };
+            self->tokens[self->size - 2] = stoken;
+        }
+        self->tokens[self->size - 1] = token;
+
+        string = w = next;
+    }
+    self->tokens = g_renew(NkToken, self->tokens, ++self->size);
+    NkToken token = {
+        .string = string
+    };
+    self->tokens[self->size - 1] = token;
+
+    return self;
+
+fail:
+    nk_token_list_unref(self);
+    return NULL;
+}
+
+static gboolean
+_nk_token_list_search_enum_tokens(NkTokenList *self, const gchar * const *tokens, guint64 size, guint64 *used_tokens, GError **error)
+{
+    gsize i;
+    for ( i = 0 ; i < self->size ; ++i )
+    {
+        if ( self->tokens[i].name == NULL )
+            continue;
+        if ( ! nk_enum_parse(self->tokens[i].name, tokens, size, FALSE, FALSE, &self->tokens[i].value) )
+        {
+            g_set_error(error, NK_TOKEN_ERROR, NK_TOKEN_ERROR_UNKNOWN_TOKEN, "Unknown token: %s", self->tokens[i].name);
+            return FALSE;
+        }
+        *used_tokens |= (1 << self->tokens[i].value);
+
+        if ( self->tokens[i].fallback != NULL )
+            _nk_token_list_search_enum_tokens(self->tokens[i].fallback, tokens, size, used_tokens, error);
+        if ( self->tokens[i].substitute != NULL )
+            _nk_token_list_search_enum_tokens(self->tokens[i].substitute, tokens, size, used_tokens, error);
+        if ( self->tokens[i].fallback != NULL )
+        {
+            NkTokenRegex *regex;
+            for ( regex = self->tokens[i].replace ; regex->regex != NULL ; ++regex )
+                _nk_token_list_search_enum_tokens(regex->replacement, tokens, size, used_tokens, error);
+        }
+    }
+    return TRUE;
+}
+
+NkTokenList *
+nk_token_list_parse_enum(gchar *string, gunichar identifier, const gchar * const *tokens, guint64 size, guint64 *ret_used_tokens, GError **error)
+{
+    g_return_val_if_fail(string != NULL, NULL);
+
+    NkTokenList *self;
+
+    self = nk_token_list_parse(string, identifier, error);
+    if ( self == NULL )
+        return NULL;
+
+    guint64 used_tokens = 0;
+    if ( ! _nk_token_list_search_enum_tokens(self, tokens, size, &used_tokens, error) )
+        goto fail;
+    if ( ret_used_tokens != NULL )
+        *ret_used_tokens = used_tokens;
+
+    return self;
+
+fail:
+    nk_token_list_unref(self);
+    return NULL;
+}
+
+NkTokenList *
+nk_token_list_ref(NkTokenList *self)
+{
+    g_return_val_if_fail(self != NULL, NULL);
+    ++self->ref_count;
+    return self;
+}
+
+static void
+_nk_token_list_free(NkTokenList *self)
+{
+
+    gsize i;
+    for ( i = 0 ; i < self->size ; ++i )
+    {
+        if ( self->tokens[i].substitute != NULL)
+            _nk_token_list_free(self->tokens[i].substitute);
+        else if ( self->tokens[i].replace != NULL )
+        {
+            NkTokenRegex *regex;
+            for ( regex = self->tokens[i].replace ; regex->regex != NULL ; ++regex )
+            {
+                g_regex_unref(regex->regex);
+                _nk_token_list_free(regex->replacement);
+            }
+            g_free(self->tokens[i].replace);
+        }
+        else if ( self->tokens[i].fallback != NULL )
+            _nk_token_list_free(self->tokens[i].fallback);
+    }
+
+    g_free(self->tokens);
+
+    g_free(self);
+}
+
+void
+nk_token_list_unref(NkTokenList *self)
+{
+    g_return_if_fail(self != NULL);
+    if ( --self->ref_count > 0 )
+        return;
+
+    g_free(self->string);
+
+    _nk_token_list_free(self);
+}
+
+static void
+_nk_token_list_replace(GString *string, const NkTokenList *self, NkTokenListReplaceCallback callback, gpointer user_data)
+{
+    gsize i;
+    for ( i = 0 ; i < self->size ; ++i )
+    {
+        if ( self->tokens[i].string != NULL )
+        {
+            g_string_append(string, self->tokens[i].string);
+            continue;
+        }
+
+        const gchar *data;
+        data = callback(self->tokens[i].name, self->tokens[i].value, self->tokens[i].key, self->tokens[i].index, user_data);
+        if ( data != NULL )
+        {
+            if ( self->tokens[i].substitute != NULL)
+                _nk_token_list_replace(string, self->tokens[i].substitute, callback, user_data);
+            else if ( self->tokens[i].replace != NULL )
+            {
+                NkTokenRegex *regex;
+                gchar *n = NULL;
+                for ( regex = self->tokens[i].replace ; regex->regex != NULL ; ++regex )
+                {
+                    gchar *tmp = n;
+                    gchar *replacement;
+                    replacement = nk_token_list_replace(regex->replacement, callback, user_data);
+                    n = g_regex_replace(regex->regex, data, -1, 0, replacement, 0, NULL);
+                    g_free(replacement);
+                    g_free(tmp);
+                    if ( n == NULL )
+                        break;
+                    data = n;
+                }
+                if ( n != NULL )
+                    g_string_append(string, n);
+                g_free(n);
+            }
+            else if ( ! self->tokens[i].no_data )
+                g_string_append(string, data);
+        }
+        else if ( self->tokens[i].fallback != NULL )
+            _nk_token_list_replace(string, self->tokens[i].fallback, callback, user_data);
+    }
+}
+
+gchar *
+nk_token_list_replace(const NkTokenList *self, NkTokenListReplaceCallback callback, gpointer user_data)
+{
+    g_return_val_if_fail(self != NULL, NULL);
+    g_return_val_if_fail(callback != NULL, NULL);
+
+    GString *string;
+    string = g_string_sized_new(self->length);
+
+    _nk_token_list_replace(string, self, callback, user_data);
+
+    return g_string_free(string, FALSE);
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/uuid-apr-util.c
@@ -0,0 +1,64 @@
+/*
+ * libnkutils/uuid - Miscellaneous utilities, uuid module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "libnkutils-uuid"
+
+#include <string.h>
+
+#include <glib.h>
+#include <apr_uuid.h>
+
+#include "nkutils-uuid.h"
+#include "uuid-internal.h"
+
+void
+nk_uuid_update_string(NkUuid *self)
+{
+    apr_uuid_format(self->string, (apr_uuid_t *) self);
+}
+
+void
+nk_uuid_generate(NkUuid *self)
+{
+    apr_uuid_get((apr_uuid_t *) self);
+    nk_uuid_update_string(self);
+}
+
+gboolean
+nk_uuid_parse(NkUuid *self, const gchar *string)
+{
+    if ( apr_uuid_parse((apr_uuid_t *) self, string) < 0 )
+        return FALSE;
+
+    nk_uuid_update_string(self);
+    return TRUE;
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/uuid-internal.h
@@ -0,0 +1,31 @@
+/*
+ * libnkutils/uuid - Miscellaneous utilities, uuid module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef __NK_UTILS_UUID_INTERNAL_H__
+#define __NK_UTILS_UUID_INTERNAL_H__
+
+void nk_uuid_update_string(NkUuid *uuid);
+
+#endif /* __NK_UTILS_UUID_INTERNAL_H__ */
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/uuid-libuuid.c
@@ -0,0 +1,64 @@
+/*
+ * libnkutils/uuid - Miscellaneous utilities, uuid module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "libnkutils-uuid"
+
+#include <string.h>
+
+#include <glib.h>
+#include <uuid.h>
+
+#include "nkutils-uuid.h"
+#include "uuid-internal.h"
+
+void
+nk_uuid_update_string(NkUuid *self)
+{
+    uuid_unparse_lower(self->data, self->string);
+}
+
+void
+nk_uuid_generate(NkUuid *self)
+{
+    uuid_generate(self->data);
+    nk_uuid_update_string(self);
+}
+
+gboolean
+nk_uuid_parse(NkUuid *self, const gchar *string)
+{
+    if ( uuid_parse(string, self->data) < 0 )
+        return FALSE;
+
+    nk_uuid_update_string(self);
+    return TRUE;
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/uuid.c
@@ -0,0 +1,70 @@
+/*
+ * libnkutils/uuid - Miscellaneous utilities, uuid module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "libnkutils-uuid"
+
+#include <string.h>
+
+#include <glib.h>
+
+#include "nkutils-uuid.h"
+#include "uuid-internal.h"
+
+#define SHA1_SIZE 20
+
+void
+nk_uuid_from_name(NkUuid *self, const gchar *name, gssize length)
+{
+    if ( length < 0 )
+        length = strlen(name);
+
+    GChecksum *c;
+    c = g_checksum_new(G_CHECKSUM_SHA1);
+    g_checksum_update(c, self->data, NK_UUID_LENGTH);
+    g_checksum_update(c, (const guchar *) name, length);
+
+    guchar sum[SHA1_SIZE];
+    gsize l = SHA1_SIZE;
+    g_checksum_get_digest(c, sum, &l);
+    g_checksum_free(c);
+
+    memcpy(self->data, sum, NK_UUID_LENGTH);
+    /* Set variant as RFC 4122 */
+    self->data[8] &= 0x3F;
+    self->data[8] |= 0x80;
+
+    /* Set version as 5, SHA-1 hash-based */
+    self->data[6] &= 0x0F;
+    self->data[6] |= (5 << 4);
+
+    nk_uuid_update_string(self);
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/xdg-de.c
@@ -0,0 +1,151 @@
+/*
+ * libnkutils/xdg-de - Miscellaneous utilities, XDG DE module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "libnkutils-xdg-de"
+
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "nkutils-enum.h"
+#include "nkutils-xdg-de.h"
+
+static const gchar * const _nk_xdg_de_current_desktop_generic_names[] = {
+    "generic",
+    NULL
+};
+
+static const gchar * const _nk_xdg_de_current_desktop_gnome_names[] = {
+    "GNOME",
+    "GNOME-Flashback",
+    NULL
+};
+
+static const gchar * const _nk_xdg_de_current_desktop_kde_names[] = {
+    "KDE",
+    NULL
+};
+
+static const gchar * const _nk_xdg_de_current_desktop_i3_names[] = {
+    "i3",
+    NULL
+};
+
+static const struct {
+    const gchar * const * const names;
+    gsize size;
+} _nk_xdg_de_current_desktop_names[] = {
+    [NK_XDG_DE_NONE] = { .names = _nk_xdg_de_current_desktop_generic_names, .size = G_N_ELEMENTS(_nk_xdg_de_current_desktop_generic_names), },
+    [NK_XDG_DE_GNOME] = { .names = _nk_xdg_de_current_desktop_gnome_names, .size = G_N_ELEMENTS(_nk_xdg_de_current_desktop_gnome_names), },
+    [NK_XDG_DE_KDE] = { .names = _nk_xdg_de_current_desktop_kde_names, .size = G_N_ELEMENTS(_nk_xdg_de_current_desktop_kde_names), },
+    [NK_XDG_DE_I3] = { .names = _nk_xdg_de_current_desktop_i3_names, .size = G_N_ELEMENTS(_nk_xdg_de_current_desktop_i3_names), },
+};
+
+static const gchar * const _nk_xdg_de_session_desktop_names[] = {
+    [NK_XDG_DE_NONE] = "X-Generic",
+    [NK_XDG_DE_GNOME] = "GNOME",
+    [NK_XDG_DE_KDE] = "KDE",
+    [NK_XDG_DE_I3] = "i3",
+};
+
+static const gchar * const _nk_xdg_de_desktop_session_names[] = {
+    [NK_XDG_DE_NONE] = "generic",
+    [NK_XDG_DE_GNOME] = "gnome",
+    [NK_XDG_DE_KDE] = "kde",
+    [NK_XDG_DE_I3] = "i3",
+};
+
+NkXdgDe
+nk_xdg_de_detect(void)
+{
+    static NkXdgDe _nk_xdg_de = NK_XDG_DE_NONE - 1;
+    if ( _nk_xdg_de != (NkXdgDe) ( NK_XDG_DE_NONE - 1 ) )
+        return _nk_xdg_de;
+
+    const gchar *var;
+    guint64 value;
+
+    var = g_getenv("XDG_CURRENT_DESKTOP");
+    if ( var != NULL )
+    {
+        const gchar *s, *e = var + strlen(var);
+        gsize i;
+        for ( ; var < e ; var = g_utf8_next_char(s) )
+        {
+            s = g_utf8_strchr(var, e - var, ':');
+            if ( s == NULL )
+                s = e;
+            gsize l = s - var + 1;
+            gchar *val = g_newa(gchar, l);
+            g_snprintf(val, l, "%s", var);
+            for ( i = 0 ; i < G_N_ELEMENTS(_nk_xdg_de_current_desktop_names) ; ++i )
+            {
+                if ( nk_enum_parse(val, _nk_xdg_de_current_desktop_names[i].names, _nk_xdg_de_current_desktop_names[i].size, FALSE, FALSE, &value) )
+                    _nk_xdg_de = i;
+            }
+        }
+        if ( _nk_xdg_de != (NkXdgDe) ( NK_XDG_DE_NONE - 1 ) )
+            return _nk_xdg_de;
+    }
+
+    var = g_getenv("XDG_SESSION_DESKTOP");
+    if ( ( var != NULL ) && nk_enum_parse(var, _nk_xdg_de_session_desktop_names, G_N_ELEMENTS(_nk_xdg_de_session_desktop_names), TRUE, TRUE, &value) )
+    {
+        _nk_xdg_de = value;
+        return _nk_xdg_de;
+    }
+
+    var = g_getenv("GNOME_DESKTOP_SESSION_ID");
+    if ( ( var != NULL ) && ( *var != '\0' ) )
+    {
+        _nk_xdg_de = NK_XDG_DE_GNOME;
+        return _nk_xdg_de;
+    }
+
+    var = g_getenv("KDE_FULL_SESSION");
+    if ( ( var != NULL ) && ( *var != '\0' ) )
+    {
+        _nk_xdg_de = NK_XDG_DE_KDE;
+        return _nk_xdg_de;
+    }
+
+    var = g_getenv("DESKTOP_SESSION");
+    if ( ( var != NULL ) && nk_enum_parse(var, _nk_xdg_de_desktop_session_names, G_N_ELEMENTS(_nk_xdg_de_desktop_session_names), FALSE, FALSE, &value) )
+    {
+        _nk_xdg_de = value;
+        return _nk_xdg_de;
+    }
+
+    _nk_xdg_de = NK_XDG_DE_NONE;
+    return _nk_xdg_de;
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/src/xdg-theme.c
@@ -0,0 +1,1123 @@
+/*
+ * libnkutils/xdg-theme - Miscellaneous utilities, XDG Theme module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif /* G_LOG_DOMAIN */
+#define G_LOG_DOMAIN "libnkutils-xdg-theme"
+
+#include <string.h>
+#include <locale.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <gio/gio.h>
+
+#include "nkutils-enum.h"
+#include "nkutils-gtk-settings.h"
+#include "nkutils-xdg-de.h"
+#include "nkutils-xdg-theme.h"
+
+typedef enum {
+    TYPE_ICON,
+    TYPE_SOUND,
+#define NUM_TYPES 2
+} NkXdgThemeThemeType;
+
+#define NK_XDG_THEME_ICON_FALLBACK_THEME "hicolor"
+#define NK_XDG_THEME_SOUND_FALLBACK_THEME "freedesktop"
+
+typedef struct {
+    const gchar *schema_id;
+    const gchar *signal_name;
+    const gchar *setting_name;
+} NkXdgThemeGnomeSettings;
+
+static const NkXdgThemeGnomeSettings _nk_xdg_theme_gnome_settings[] = {
+    [TYPE_ICON] = {
+        .schema_id = "org.gnome.desktop.interface",
+        .signal_name = "changed::icon-theme",
+        .setting_name = "icon-theme",
+    },
+    [TYPE_SOUND] = {
+        .schema_id = "org.gnome.desktop.sound",
+        .signal_name = "changed::theme-name",
+        .setting_name = "theme-name",
+    },
+};
+
+typedef struct {
+    NkXdgThemeThemeType type;
+    gchar **dirs;
+    gsize dirs_length;
+    const gchar * const *fallback_themes;
+    GHashTable *themes;
+    gchar *de_theme;
+    gpointer de_data;
+    GDestroyNotify de_notify;
+    gchar *gtk_theme;
+} NkXdgThemeTypeContext;
+
+struct _NkXdgThemeContext {
+    NkXdgThemeTypeContext types[NUM_TYPES];
+};
+
+typedef struct {
+    NkXdgThemeTypeContext *context;
+    gchar *name;
+    GList *subdirs;
+    GList *inherits;
+} NkXdgThemeTheme;
+
+typedef gboolean (*NkXdgThemeForeachCallback)(NkXdgThemeTheme *theme, gconstpointer user_data, gpointer *ret);
+
+typedef gboolean (*NkXdgThemeFindFileCallback)(NkXdgThemeTheme *self, const gchar * const *names, gconstpointer data, gchar **ret);
+
+typedef struct {
+    const gchar **names;
+    NkXdgThemeFindFileCallback find_file;
+    gconstpointer user_data;
+} NkXdgThemeSearchThemeData;
+
+typedef enum {
+    ICONDIR_TYPE_THRESHOLD = 0,
+    ICONDIR_TYPE_FIXED,
+    ICONDIR_TYPE_SCALABLE,
+} NkXdgThemeIconDirType;
+
+typedef enum {
+    ICONDIR_CONTEXT_CUSTOM = -1,
+    ICONDIR_CONTEXT_UNKNOWN = 0,
+    ICONDIR_CONTEXT_ACTIONS,
+    ICONDIR_CONTEXT_ANIMATIONS,
+    ICONDIR_CONTEXT_APPLICATIONS,
+    ICONDIR_CONTEXT_CATEGORIES,
+    ICONDIR_CONTEXT_DEVICES,
+    ICONDIR_CONTEXT_EMBLEMS,
+    ICONDIR_CONTEXT_EMOTES,
+    ICONDIR_CONTEXT_FILESYSTEMS,
+    ICONDIR_CONTEXT_INTERNATIONAL,
+    ICONDIR_CONTEXT_MIMETYPES,
+    ICONDIR_CONTEXT_PLACES,
+    ICONDIR_CONTEXT_STATUS,
+    ICONDIR_CONTEXT_STOCK,
+} NkXdgThemeIconDirContext;
+
+static const gchar * const _nk_xdg_theme_icon_dir_type_names[] = {
+    [ICONDIR_TYPE_THRESHOLD] = "Threshold",
+    [ICONDIR_TYPE_FIXED]     = "Fixed",
+    [ICONDIR_TYPE_SCALABLE]  = "Scalable",
+};
+
+static const gchar * const _nk_xdg_theme_icon_dir_context_names[] = {
+    [ICONDIR_CONTEXT_UNKNOWN] = NULL,
+    [ICONDIR_CONTEXT_ACTIONS]       = "Actions",
+    [ICONDIR_CONTEXT_ANIMATIONS]    = "Animations",
+    [ICONDIR_CONTEXT_APPLICATIONS]  = "Applications",
+    [ICONDIR_CONTEXT_CATEGORIES]    = "Categories",
+    [ICONDIR_CONTEXT_DEVICES]       = "Devices",
+    [ICONDIR_CONTEXT_EMBLEMS]       = "Emblems",
+    [ICONDIR_CONTEXT_EMOTES]        = "Emotes",
+    [ICONDIR_CONTEXT_FILESYSTEMS]   = "FileSystems",
+    [ICONDIR_CONTEXT_INTERNATIONAL] = "International",
+    [ICONDIR_CONTEXT_MIMETYPES]     = "MimeTypes",
+    [ICONDIR_CONTEXT_PLACES]        = "Places",
+    [ICONDIR_CONTEXT_STATUS]        = "Status",
+    [ICONDIR_CONTEXT_STOCK]         = "Stock",
+};
+
+typedef struct {
+    NkXdgThemeIconDirContext context;
+    const gchar *context_custom;
+    gint size;
+    gint scale;
+    const gchar **extensions;
+} NkXdgThemeIconFindData;
+
+typedef struct {
+    gchar **paths;
+    gint weight;
+} NkXdgThemeDir;
+
+typedef struct {
+    NkXdgThemeDir base;
+    NkXdgThemeIconDirType type;
+    gint size;
+    gint scale;
+    gint min;
+    gint max;
+    NkXdgThemeIconDirContext context;
+    gchar *context_custom;
+} NkXdgThemeIconDir;
+
+typedef struct {
+    NkXdgThemeDir base;
+    gchar *profile;
+} NkXdgThemeSoundDir;
+
+static const gchar * const _nk_xdg_theme_empty_fallback[] = { NULL };
+
+static const gchar * const _nk_xdg_theme_subdirs[] = {
+    [TYPE_ICON] = "icons",
+    [TYPE_SOUND] = "sounds",
+};
+
+static const gchar * const _nk_xdg_theme_sections[] = {
+    [TYPE_ICON] = "Icon Theme",
+    [TYPE_SOUND] = "Sound Theme",
+};
+
+static const gchar *_nk_xdg_theme_icon_extensions[] = {
+    ".svg",
+    ".png",
+    ".xpm",
+    NULL
+};
+
+static const gchar *_nk_xdg_theme_icon_symbolic_extensions[] = {
+    ".svg",
+    ".png",
+    ".symbolic.png",
+    ".xpm",
+    NULL
+};
+
+static const gchar *_nk_xdg_theme_sound_extensions[] = {
+    ".disabled",
+    ".oga",
+    ".ogg",
+    ".wav",
+    NULL
+};
+
+static void
+_nk_xdg_theme_de_theme_gsettings_update(NkXdgThemeTypeContext *self, G_GNUC_UNUSED gchar *key, GSettings *settings)
+{
+    g_free(self->de_theme);
+    self->de_theme = g_settings_get_string(settings, "icon-theme");
+}
+
+static void
+_nk_xdg_theme_de_theme_hook(NkXdgThemeTypeContext *self)
+{
+    switch ( nk_xdg_de_detect() )
+    {
+    case NK_XDG_DE_NONE:
+    case NK_XDG_DE_I3:
+    break;
+    case NK_XDG_DE_GNOME:
+    {
+        const NkXdgThemeGnomeSettings *settings = &_nk_xdg_theme_gnome_settings[self->type];
+        GSettingsSchemaSource *schema_source;
+        GSettingsSchema *schema;
+
+        schema_source = g_settings_schema_source_get_default();
+        if ( schema_source == NULL )
+            return;
+
+        schema = g_settings_schema_source_lookup(schema_source, settings->schema_id, TRUE);
+        if ( schema == NULL )
+            return;
+
+        self->de_data = g_settings_new_full(schema, NULL, NULL);
+        g_settings_schema_unref(schema);
+
+        g_signal_connect_swapped(self->de_data, settings->signal_name, G_CALLBACK(_nk_xdg_theme_de_theme_gsettings_update), self);
+        self->de_theme = g_settings_get_string(self->de_data, settings->setting_name);
+        self->de_notify = g_object_unref;
+    }
+    break;
+    case NK_XDG_DE_KDE:
+        switch ( self->type )
+        {
+        case TYPE_ICON:
+        {
+            gchar *path;
+            GKeyFile *kdeglobals;
+
+            path = g_build_filename(g_get_user_config_dir(), "kdeglobals", NULL);
+            kdeglobals = g_key_file_new();
+
+            if ( g_key_file_load_from_file(kdeglobals, path, G_KEY_FILE_NONE, NULL) )
+                self->de_theme = g_key_file_get_string(kdeglobals, "Icons", "Theme", NULL);
+            g_key_file_free(kdeglobals);
+            g_free(path);
+        }
+        break;
+        case TYPE_SOUND:
+        break;
+        }
+    break;
+    }
+
+    const gchar *gtk_settings_keys[NK_GTK_SETTINGS_NUM_VERSION] = {
+        "gtk-icon-theme-name",
+        "gtk-icon-theme-name",
+    };
+    nk_gtk_settings_get_string(&self->gtk_theme, gtk_settings_keys);
+}
+
+static void
+_nk_xdg_theme_find_dirs(NkXdgThemeTypeContext *self)
+{
+    const gchar *subdir = _nk_xdg_theme_subdirs[self->type];
+    gchar **dirs;
+    gsize length = 0, current = 0;
+    const gchar * const *system_dirs;
+    system_dirs = g_get_system_data_dirs();
+
+    while ( system_dirs[length] != NULL ) ++length;
+    ++length; /* user dir */
+    switch ( self->type )
+    {
+    case TYPE_ICON:
+        length *= 2; /* ~/.icons and pixmaps */
+    break;
+    case TYPE_SOUND:
+    break;
+    }
+    ++length; /* NULL */
+
+    dirs = g_new(gchar *, length);
+
+#define try_dir(dir) G_STMT_START { \
+        gchar *_tmp_dir = g_build_filename(dir, subdir, NULL); \
+        if ( g_file_test(_tmp_dir, G_FILE_TEST_IS_DIR) ) \
+            dirs[current++] = _tmp_dir;\
+        else \
+            g_free(_tmp_dir);\
+    } G_STMT_END
+
+    try_dir(g_get_user_data_dir());
+
+    switch ( self->type )
+    {
+    case TYPE_ICON:
+        subdir = ".icons";
+        try_dir(g_get_home_dir());
+        subdir = _nk_xdg_theme_subdirs[self->type];
+    break;
+    case TYPE_SOUND:
+    break;
+    }
+
+    const gchar * const *system_dir;
+    for ( system_dir = system_dirs ; *system_dir != NULL ; ++system_dir )
+        try_dir(*system_dir);
+
+    switch ( self->type )
+    {
+    case TYPE_ICON:
+        /* Let’s trust system_dirs to contains /usr/share, if it doesn’t,
+         * that probably means it would be useless to check it anyway */
+        subdir = "pixmaps";
+        for ( system_dir = system_dirs ; *system_dir != NULL ; ++system_dir )
+            try_dir(*system_dir);
+    break;
+    case TYPE_SOUND:
+    break;
+    }
+
+    dirs[current] = NULL;
+
+#undef try_dir
+
+    if ( current == 0 )
+    {
+        g_free(dirs);
+        return;
+    }
+    self->dirs = dirs;
+    self->dirs_length = current;
+}
+
+static gpointer
+_nk_xdg_theme_icon_subdir_new(GKeyFile *file, const gchar *subdir)
+{
+    GError *error = NULL;
+    gint size;
+    size = g_key_file_get_integer(file, subdir, "Size", &error);
+    if ( error != NULL )
+    {
+        g_clear_error(&error);
+        return NULL;
+    }
+    gint scale;
+    scale = g_key_file_get_integer(file, subdir, "Scale", &error);
+    if ( error != NULL )
+    {
+        scale = 1;
+        g_clear_error(&error);
+    }
+
+    NkXdgThemeIconDir *self;
+    self = g_slice_new0(NkXdgThemeIconDir);
+
+    self->size = size * scale;
+    self->scale = scale;
+    self->min = self->size;
+    self->max = self->size;
+
+    gchar *type;
+    type = g_key_file_get_string(file, subdir, "Type", NULL);
+    if ( type != NULL )
+    {
+        guint64 value;
+        if ( nk_enum_parse(type, _nk_xdg_theme_icon_dir_type_names, G_N_ELEMENTS(_nk_xdg_theme_icon_dir_type_names), TRUE, FALSE, &value) )
+            self->type = value;
+        g_free(type);
+    }
+
+    switch ( self->type )
+    {
+    case ICONDIR_TYPE_THRESHOLD:
+    {
+        gint threshold;
+        threshold = g_key_file_get_integer(file, subdir, "Threshold", &error);
+        if ( error != NULL )
+        {
+            threshold = 2;
+            g_clear_error(&error);
+        }
+        threshold *= scale;
+        self->min -= threshold;
+        self->max += threshold;
+        self->base.weight = (G_MININT>>2) + self->size + 1; /* So that Threshold size comes just before same Fixed */
+    }
+    break;
+    case ICONDIR_TYPE_FIXED:
+        self->base.weight = (G_MININT>>2) + self->size;
+    break;
+    case ICONDIR_TYPE_SCALABLE:
+    {
+        gint limit;
+
+        limit = g_key_file_get_integer(file, subdir, "MinSize", &error);
+        if ( error == NULL )
+            self->min = limit * scale;
+        else
+            g_clear_error(&error);
+
+        limit = g_key_file_get_integer(file, subdir, "MaxSize", &error);
+        if ( error == NULL )
+            self->max = limit * scale;
+        else
+            g_clear_error(&error);
+
+        self->base.weight = ( ( self->max - self->min ) << 4 ) / self->size;
+    }
+    break;
+    default:
+        g_slice_free(NkXdgThemeIconDir, self);
+        g_return_val_if_reached(NULL);
+    }
+    if ( self->max < self->min )
+    {
+        g_slice_free(NkXdgThemeIconDir, self);
+        return NULL;
+    }
+
+    gchar *context;
+    context = g_key_file_get_string(file, subdir, "Context", NULL);
+    if ( context != NULL )
+    {
+        guint64 value;
+        if ( nk_enum_parse(context, _nk_xdg_theme_icon_dir_context_names, G_N_ELEMENTS(_nk_xdg_theme_icon_dir_context_names), TRUE, FALSE, &value) )
+        {
+            self->context = value;
+            g_free(context);
+        }
+        else
+        {
+            self->context = ICONDIR_CONTEXT_CUSTOM;
+            self->context_custom = context;
+        }
+    }
+
+    return self;
+}
+
+static void
+_nk_xdg_theme_icon_subdir_free(gpointer data)
+{
+    NkXdgThemeIconDir *self = data;
+    g_free(self->context_custom);
+    g_strfreev(self->base.paths);
+    g_slice_free(NkXdgThemeIconDir, self);
+}
+
+static gpointer
+_nk_xdg_theme_sound_subdir_new(GKeyFile *file, const gchar *subdir)
+{
+    NkXdgThemeSoundDir *self;
+    self = g_slice_new0(NkXdgThemeSoundDir);
+
+    self->profile = g_key_file_get_string(file, subdir, "OutputProfile", NULL);
+
+    return self;
+}
+
+static void
+_nk_xdg_theme_sound_subdir_free(gpointer data)
+{
+    NkXdgThemeSoundDir *self = data;
+
+    g_free(self->profile);
+
+    g_strfreev(self->base.paths);
+    g_slice_free(NkXdgThemeSoundDir, self);
+}
+
+static gint
+_nk_xdg_theme_subdir_sort(gconstpointer a_, gconstpointer b_)
+{
+    const NkXdgThemeDir *a = a_;
+    const NkXdgThemeDir *b = b_;
+    return ( b->weight - a->weight );
+}
+
+static gpointer _nk_xdg_theme_get_theme(NkXdgThemeTypeContext *self, const gchar *name);
+static gboolean
+_nk_xdg_theme_find(NkXdgThemeTheme *self)
+{
+    const gchar *section = _nk_xdg_theme_sections[self->context->type];
+    gchar **dirs = self->context->dirs;
+    GKeyFile *file;
+
+    if ( dirs == NULL )
+        return FALSE;
+
+    file = g_key_file_new();
+    g_key_file_set_list_separator(file, ',');
+
+    gboolean found = FALSE;
+    gchar **dir;
+    for ( dir = dirs ; ( ! found ) && ( *dir != NULL ) ; ++dir )
+    {
+        gchar *filename;
+        filename = g_build_filename(*dir, self->name, "index.theme", NULL);
+        if ( g_key_file_load_from_file(file, filename, G_KEY_FILE_NONE, NULL)
+             && g_key_file_has_group(file, section) )
+            found = TRUE;
+        g_free(filename);
+    }
+
+    if ( ! found )
+        goto error;
+    found = FALSE;
+
+    gchar **subdirs, **subdir_path;
+    subdirs = g_key_file_get_string_list(file, section, "Directories", NULL, NULL);
+    if ( subdirs == NULL )
+        goto error;
+    gpointer (*subdir_new)(GKeyFile *file, const gchar *subdir);
+    void (*subdir_free)(gpointer subdir);
+    switch ( self->context->type )
+    {
+    case TYPE_ICON:
+        subdir_new = _nk_xdg_theme_icon_subdir_new;
+        subdir_free = _nk_xdg_theme_icon_subdir_free;
+    break;
+    case TYPE_SOUND:
+        subdir_new = _nk_xdg_theme_sound_subdir_new;
+        subdir_free = _nk_xdg_theme_sound_subdir_free;
+    break;
+    default:
+        g_return_val_if_reached(FALSE);
+    }
+
+    for ( subdir_path = subdirs ; *subdir_path != NULL ; ++subdir_path )
+    {
+        if ( g_key_file_has_group(file, *subdir_path) )
+        {
+            NkXdgThemeDir *subdir;
+            subdir = subdir_new(file, *subdir_path);
+            if ( subdir == NULL )
+                continue;
+
+            gsize i;
+            subdir->paths = g_new(gchar *, self->context->dirs_length + 1);
+
+            gchar **dir_;
+            for ( dir_ = self->context->dirs, i = 0 ; *dir_ != NULL ; ++dir_ )
+            {
+                gchar *path;
+                path = g_build_filename(*dir_, self->name, *subdir_path, NULL);
+                if ( g_file_test(path, G_FILE_TEST_IS_DIR) )
+                    subdir->paths[i++] = path;
+                else
+                    g_free(path);
+            }
+            subdir->paths[i] = NULL;
+
+            if ( i == 0 )
+                subdir_free(subdir);
+            else
+                self->subdirs = g_list_insert_sorted(self->subdirs, subdir, _nk_xdg_theme_subdir_sort);
+        }
+    }
+
+    if ( self->subdirs == NULL )
+        goto error;
+
+    gchar **inherits;
+    inherits = g_key_file_get_string_list(file, section, "Inherits", NULL, NULL);
+    if ( inherits != NULL )
+    {
+        gchar **inherit;
+        for ( inherit = inherits ; *inherit != NULL ; ++inherit )
+        {
+            gpointer inherited;
+            inherited = _nk_xdg_theme_get_theme(self->context, *inherit);
+            if ( inherited != NULL )
+                self->inherits = g_list_prepend(self->inherits, inherited);
+        }
+        g_strfreev(inherits);
+        self->inherits = g_list_reverse(self->inherits);
+    }
+
+    found = TRUE;
+error:
+    g_key_file_free(file);
+    return found;
+}
+
+static NkXdgThemeTheme *
+_nk_xdg_theme_load_theme(NkXdgThemeTypeContext *context, const gchar *name)
+{
+    NkXdgThemeTheme *self;
+    self = g_new0(NkXdgThemeTheme, 1);
+    self->context = context;
+    self->name = g_strdup(name);
+
+    /*
+     * Make sure we won’t recursively try to load a theme.
+     * Some themes (like Numix as of early 2018) inherit themselves
+     * and put us in an infinite recursion.
+     */
+    g_hash_table_insert(context->themes, self->name, NULL);
+
+    if ( ! _nk_xdg_theme_find(self) )
+    {
+        g_free(self);
+        return NULL;
+    }
+
+    g_hash_table_steal(context->themes, self->name);
+    g_hash_table_insert(context->themes, self->name, self);
+    return self;
+}
+
+static void
+_nk_xdg_theme_theme_free(gpointer data)
+{
+    NkXdgThemeTheme *self = data;
+    if ( self == NULL )
+        return;
+
+    void (*subdir_free)(gpointer subdir);
+    switch ( self->context->type )
+    {
+    case TYPE_ICON:
+        subdir_free = _nk_xdg_theme_icon_subdir_free;
+    break;
+    case TYPE_SOUND:
+        subdir_free = _nk_xdg_theme_sound_subdir_free;
+    break;
+    default:
+        g_return_if_reached();
+    }
+
+    g_list_free_full(self->subdirs, subdir_free);
+    g_list_free(self->inherits);
+    g_free(self);
+}
+
+static gpointer
+_nk_xdg_theme_get_theme(NkXdgThemeTypeContext *self, const gchar *name)
+{
+    if ( name == NULL )
+        return NULL;
+
+    NkXdgThemeTheme *theme;
+    if ( g_hash_table_lookup_extended(self->themes, name, NULL, (gpointer *) &theme) )
+        return theme;
+
+    return _nk_xdg_theme_load_theme(self, name);
+}
+
+NkXdgThemeContext *
+nk_xdg_theme_context_new(const gchar * const *icon_fallback_themes, const gchar * const *sound_fallback_themes)
+{
+    const gchar * const *fallbacks[NUM_TYPES] = {
+        [TYPE_ICON] = icon_fallback_themes,
+        [TYPE_SOUND] = sound_fallback_themes,
+    };
+
+    NkXdgThemeContext *context;
+    context = g_new0(NkXdgThemeContext, 1);
+
+    NkXdgThemeThemeType type;
+    for ( type = 0 ; type < NUM_TYPES ; ++type )
+    {
+        NkXdgThemeTypeContext *self = &context->types[type];
+        self->type = type;
+        _nk_xdg_theme_find_dirs(self);
+        self->themes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, _nk_xdg_theme_theme_free);
+        self->fallback_themes = ( fallbacks[self->type] != NULL ) ? fallbacks[self->type] : _nk_xdg_theme_empty_fallback;
+        _nk_xdg_theme_de_theme_hook(self);
+    }
+
+    return context;
+}
+
+void
+nk_xdg_theme_context_free(NkXdgThemeContext *context)
+{
+    NkXdgThemeThemeType type;
+    for ( type = 0 ; type < NUM_TYPES ; ++type )
+    {
+        NkXdgThemeTypeContext *self = &context->types[type];
+
+        g_free(self->gtk_theme);
+        if ( self->de_notify != NULL )
+            self->de_notify(self->de_data);
+        g_free(self->de_theme);
+        g_hash_table_unref(self->themes);
+        g_strfreev(self->dirs);
+    }
+
+    g_free(context);
+}
+
+static gboolean
+_nk_xdg_theme_get_file(NkXdgThemeTheme *self, const gchar **names, NkXdgThemeFindFileCallback find_file, gconstpointer data, gchar **ret)
+{
+    if ( find_file(self, names, data, ret) )
+        return TRUE;
+
+    GList *inherited;
+    for ( inherited = self->inherits ; inherited != NULL ; inherited = g_list_next(inherited) )
+    {
+        if ( _nk_xdg_theme_get_file(inherited->data, names, find_file, data, ret) )
+            return TRUE;
+    }
+    return FALSE;
+}
+
+static gboolean
+_nk_xdg_theme_foreach_theme(NkXdgThemeTypeContext *self, const gchar * const *theme_names, const gchar *fallback_theme,  NkXdgThemeForeachCallback callback, gconstpointer data, gpointer *ret)
+{
+    NkXdgThemeTheme *theme;
+
+    const gchar * const *theme_name;
+    for ( theme_name = theme_names ; *theme_name != NULL ; ++theme_name )
+    {
+        theme = _nk_xdg_theme_get_theme(self, *theme_name);
+        if ( ( theme != NULL ) && callback(theme, data, ret) )
+            return TRUE;
+    }
+
+    if ( self->de_theme != NULL )
+    {
+        theme = _nk_xdg_theme_get_theme(self, self->de_theme);
+        if ( ( theme != NULL ) && callback(theme, data, ret) )
+            return TRUE;
+    }
+
+    if ( self->gtk_theme != NULL )
+    {
+        theme = _nk_xdg_theme_get_theme(self, self->gtk_theme);
+        if ( ( theme != NULL ) && callback(theme, data, ret) )
+            return TRUE;
+    }
+
+    for ( theme_name = self->fallback_themes ; *theme_name != NULL ; ++theme_name )
+    {
+        theme = _nk_xdg_theme_get_theme(self, *theme_name);
+        if ( ( theme != NULL ) && callback(theme, data, ret) )
+            return TRUE;
+    }
+
+    theme = _nk_xdg_theme_get_theme(self, fallback_theme);
+    if ( ( theme != NULL ) && callback(theme, data, ret) )
+        return TRUE;
+
+    return FALSE;
+}
+
+static gboolean
+_nk_xdg_theme_search_theme(NkXdgThemeTheme *theme, gconstpointer user_data, gpointer *ret)
+{
+    const NkXdgThemeSearchThemeData *data = user_data;
+
+    return _nk_xdg_theme_get_file(theme, data->names, data->find_file, data->user_data, (gchar **) ret);
+}
+
+static gchar *
+_nk_xdg_theme_search_themes(NkXdgThemeTypeContext *self, const gchar **names, const gchar * const *theme_names, const gchar *fallback_theme, NkXdgThemeFindFileCallback find_file, gconstpointer user_data)
+{
+    NkXdgThemeSearchThemeData data = {
+        .names = names,
+        .find_file = find_file,
+        .user_data = user_data,
+    };
+
+    gchar *file = NULL;
+
+    _nk_xdg_theme_foreach_theme(self, theme_names, fallback_theme, _nk_xdg_theme_search_theme, &data, (gpointer *) &file);
+
+    return file;
+}
+
+static gboolean
+_nk_xdg_theme_try_file(const gchar *dir, const gchar *name, const gchar * const *extensions, gchar **ret)
+{
+    gsize i;
+    for ( i = 0 ; extensions[i] != NULL ; ++i )
+    {
+        gchar *file;
+        file = g_strconcat(dir, G_DIR_SEPARATOR_S, name, extensions[i], NULL);
+        if ( g_file_test(file, G_FILE_TEST_IS_REGULAR) )
+        {
+            *ret = file;
+            return TRUE;
+        }
+        g_free(file);
+    }
+    return FALSE;
+}
+
+static gboolean
+_nk_xdg_theme_try_fallback_internal(gchar **dir, const gchar *name, const gchar * const *extensions, gchar **ret)
+{
+    if ( dir == NULL )
+        return FALSE;
+
+    for ( ; *dir != NULL ; ++dir )
+    {
+        if ( _nk_xdg_theme_try_file(*dir, name, extensions, ret) )
+            return TRUE;
+    }
+    return FALSE;
+}
+
+static gboolean
+_nk_xdg_theme_try_fallback(gchar **dirs, const gchar * const *theme_names, const gchar *name, const gchar * const *extensions, gchar **ret)
+{
+    if ( theme_names != NULL )
+    {
+        const gchar * const *theme_name;
+        gsize l = 0, tl;
+        for ( theme_name = theme_names ; *theme_name != NULL ; ++theme_name )
+        {
+            tl = strlen(*theme_name);
+            if ( tl > l )
+                l = tl;
+        }
+        l += strlen(G_DIR_SEPARATOR_S) + strlen(name) + 1;
+
+        gchar *themed_name = NULL;
+        themed_name = g_newa(gchar, l);
+        for ( theme_name = theme_names ; *theme_name != NULL ; ++theme_name )
+        {
+            g_snprintf(themed_name, l, "%s%c%s", *theme_name, G_DIR_SEPARATOR, name);
+            if ( _nk_xdg_theme_try_fallback_internal(dirs, themed_name, extensions, ret) )
+                return TRUE;
+        }
+    }
+
+    return _nk_xdg_theme_try_fallback_internal(dirs, name, extensions, ret);
+}
+
+static gchar *
+_nk_xdg_theme_search_file(NkXdgThemeTypeContext *self, const gchar **names, const gchar * const *theme_names, const gchar *fallback_theme, NkXdgThemeFindFileCallback find_file, gconstpointer data, const gchar * const *extensions)
+{
+    gchar *file;
+
+    file = _nk_xdg_theme_search_themes(self, names, theme_names, fallback_theme, find_file, data);
+    if ( file != NULL )
+        return file;
+
+    const gchar * const *subname;
+    for ( subname = names ; *subname != NULL ; ++subname )
+    {
+        if ( _nk_xdg_theme_try_fallback(self->dirs, theme_names, *subname, extensions, &file) )
+            return file;
+    }
+    return NULL;
+}
+
+static gboolean
+_nk_xdg_theme_foreach_noop(G_GNUC_UNUSED NkXdgThemeTheme *theme, G_GNUC_UNUSED gconstpointer user_data, G_GNUC_UNUSED gpointer *ret)
+{
+    return FALSE;
+}
+
+void
+nk_xdg_theme_preload_themes_icon(NkXdgThemeContext *context, const gchar * const *theme_names)
+{
+    g_return_if_fail(context != NULL);
+
+    NkXdgThemeTypeContext *self = &context->types[TYPE_ICON];
+
+    _nk_xdg_theme_foreach_theme(self, theme_names, NK_XDG_THEME_ICON_FALLBACK_THEME, _nk_xdg_theme_foreach_noop, NULL, NULL);
+}
+
+static gint
+_nk_xdg_theme_icon_subdir_compute_distance(NkXdgThemeIconDir *self, gint size)
+{
+    if ( self->type == ICONDIR_TYPE_FIXED )
+        return ABS(self->size - size);
+    if ( size < self->min )
+        return self->min - size;
+    if ( size > self->max )
+        return size - self->max;
+    return 0;
+}
+
+static gboolean
+_nk_xdg_theme_icon_find_file(NkXdgThemeTheme *self, const gchar * const *names, gconstpointer user_data, gchar **ret)
+{
+    const NkXdgThemeIconFindData *data = user_data;
+    const gchar *name = *names;
+
+    gint best_distance = G_MAXINT;
+    gchar *best_file = NULL;
+
+    GList *subdir_;
+    for ( subdir_ = self->subdirs ; subdir_ != NULL ; subdir_ = g_list_next(subdir_) )
+    {
+        NkXdgThemeIconDir *subdir = subdir_->data;
+        gchar **path;
+
+        if ( ( data->context != ICONDIR_CONTEXT_UNKNOWN ) && ( subdir->context != ICONDIR_CONTEXT_UNKNOWN ) )
+        {
+            if ( data->context != subdir->context )
+                continue;
+            if ( ( data->context == ICONDIR_CONTEXT_CUSTOM ) && ( g_ascii_strcasecmp(data->context_custom, subdir->context_custom) != 0 ) )
+                continue;
+        }
+
+        gboolean try_best = ( ( data->size > 0 ) && ( ( data->scale != subdir->scale ) || ( data->size < subdir->min ) || ( data->size > subdir->max ) ) );
+
+        for ( path = subdir->base.paths ; *path != NULL ; ++path )
+        {
+            gchar *file;
+            if ( _nk_xdg_theme_try_file(*path, name, data->extensions, &file) )
+            {
+                if ( try_best )
+                {
+                    gint distance;
+                    distance = _nk_xdg_theme_icon_subdir_compute_distance(subdir, data->size);
+                    if ( distance < best_distance )
+                    {
+                        g_free(best_file);
+                        best_file = file;
+                        best_distance = distance;
+                    }
+                    else
+                        g_free(file);
+                }
+                else
+                {
+                    *ret = file;
+                    return TRUE;
+                }
+            }
+        }
+    }
+
+    if ( best_file == NULL )
+        return FALSE;
+
+    *ret = best_file;
+    return TRUE;
+}
+
+gchar *
+nk_xdg_theme_get_icon(NkXdgThemeContext *context, const gchar * const *theme_names, const gchar *context_name, const gchar *name, gint size, gint scale, gboolean svg)
+{
+    g_return_val_if_fail(context != NULL, NULL);
+    g_return_val_if_fail(name != NULL, NULL);
+    g_return_val_if_fail(scale > 0, NULL);
+
+    NkXdgThemeTypeContext *self = &context->types[TYPE_ICON];
+
+    gboolean symbolic = g_str_has_suffix(name, "-symbolic");
+    guint64 value;
+    NkXdgThemeIconFindData data = {
+        .context = ICONDIR_CONTEXT_CUSTOM,
+        .context_custom = context_name,
+        .size = size * scale,
+        .scale = scale,
+        .extensions = ( symbolic ? _nk_xdg_theme_icon_symbolic_extensions : _nk_xdg_theme_icon_extensions ) + ( svg ? 0 : 1 ),
+    };
+    if ( nk_enum_parse(context_name, _nk_xdg_theme_icon_dir_context_names, G_N_ELEMENTS(_nk_xdg_theme_icon_dir_context_names), TRUE, FALSE, &value) )
+        data.context = value;
+
+    gchar *file;
+    const gchar *names[] = { name, NULL };
+
+    file = _nk_xdg_theme_search_file(self, names, theme_names, NK_XDG_THEME_ICON_FALLBACK_THEME, _nk_xdg_theme_icon_find_file, &data, data.extensions);
+    if ( file != NULL )
+        return file;
+
+    if ( symbolic )
+    {
+        gchar *no_symbolic_name;
+        gsize l;
+        l = strlen(name) - strlen("-symbolic") + 1;
+        no_symbolic_name = g_newa(gchar, l);
+        g_snprintf(no_symbolic_name, l, "%s", name);
+        return nk_xdg_theme_get_icon(context, theme_names, context_name, no_symbolic_name, size, scale, svg);
+    }
+
+    return NULL;
+}
+
+static gboolean
+_nk_xdg_theme_sound_find_file(NkXdgThemeTheme *self, const gchar * const *names, gconstpointer user_data, gchar **ret)
+{
+    const gchar *profile = user_data;
+    GList *subdir_;
+    for ( subdir_ = self->subdirs ; subdir_ != NULL ; subdir_ = g_list_next(subdir_) )
+    {
+        NkXdgThemeSoundDir *subdir = subdir_->data;
+        gchar **path;
+        if ( g_strcmp0(profile, subdir->profile) != 0 )
+            continue;
+
+        for ( path = subdir->base.paths ; *path != NULL ; ++path )
+        {
+            const gchar * const *name;
+            for ( name = names ; *name != NULL ; ++name )
+            {
+                if ( _nk_xdg_theme_try_file(*path, *name, _nk_xdg_theme_sound_extensions, ret) )
+                    return TRUE;
+            }
+        }
+    }
+
+    if ( profile == NULL )
+        return FALSE;
+
+    if ( g_strcmp0(profile, "stereo") == 0 )
+        return _nk_xdg_theme_sound_find_file(self, names, NULL, ret);
+
+    return _nk_xdg_theme_sound_find_file(self, names, "stereo", ret);
+}
+
+void
+nk_xdg_theme_preload_themes_sound(NkXdgThemeContext *context, const gchar * const *theme_names)
+{
+    g_return_if_fail(context != NULL);
+
+    NkXdgThemeTypeContext *self = &context->types[TYPE_SOUND];
+
+    _nk_xdg_theme_foreach_theme(self, theme_names, NK_XDG_THEME_SOUND_FALLBACK_THEME, _nk_xdg_theme_foreach_noop, NULL, NULL);
+}
+
+gchar *
+nk_xdg_theme_get_sound(NkXdgThemeContext *context, const gchar * const *theme_names, const gchar *name, const gchar *profile, const gchar *locale)
+{
+    g_return_val_if_fail(context != NULL, NULL);
+    g_return_val_if_fail(name != NULL, NULL);
+
+    NkXdgThemeTypeContext *self = &context->types[TYPE_SOUND];
+
+    const gchar *c;
+    gsize l;
+
+#ifdef G_OS_WIN32
+    gchar *locale_ = NULL;
+#endif /* G_OS_WIN32 */
+    if ( locale == NULL )
+#ifdef G_OS_WIN32
+        locale = locale_ = g_win32_getlocale();
+#else /* ! G_OS_WIN32 */
+        locale = setlocale(LC_MESSAGES, NULL);
+#endif /* ! G_OS_WIN32 */
+    gchar *locales[5];
+    gsize locales_count = 0;
+
+    if ( ( *locale != '\0' ) && ( g_strcmp0(locale, "C") != 0 ) )
+    {
+        l = strlen(locale);
+        locales[locales_count] = g_newa(gchar, l + 2);
+        g_snprintf(locales[locales_count++], l + 2, "%s" G_DIR_SEPARATOR_S, locale);
+        if ( ( c = g_utf8_strchr(locale, -1, '@') ) != NULL )
+        {
+            l = (c - locale);
+
+            locales[locales_count] = g_newa(gchar, l + 2);
+            g_snprintf(locales[locales_count++], l + 2, "%.*s" G_DIR_SEPARATOR_S, (gint) l, locale);
+        }
+        if ( ( c = g_utf8_strchr(locale, -1, '_') ) != NULL )
+        {
+            l = (c - locale);
+            locales[locales_count] = g_newa(gchar, l + 1);
+            g_snprintf(locales[locales_count++], l + 2, "%.*s" G_DIR_SEPARATOR_S, (gint) l, locale);
+        }
+    }
+    locales[locales_count++] = "C" G_DIR_SEPARATOR_S;
+    locales[locales_count++] = "";
+
+#ifdef G_OS_WIN32
+    g_free(locale_);
+#endif /* G_OS_WIN32 */
+
+    gsize variants_count = 1;
+    l = strlen(name);
+    for ( c = name ; ( c = g_utf8_strchr(c, l - (c - name), '-') ) != NULL ; ++c )
+        ++variants_count;
+
+    const gchar **names;
+    names = g_newa(const gchar *, ( locales_count * variants_count ) + 1);
+    names[locales_count * variants_count] = NULL;
+
+    gsize i, j;
+    for ( i = 0 ; i < locales_count ; ++i )
+    {
+        gsize ll;
+        ll = strlen(locales[i]);
+
+        for ( c = name + l, j = 0 ; j < variants_count ; c = g_utf8_strrchr(name, c - name, '-'), ++j )
+        {
+            g_assert_nonnull(c);
+
+            gsize sl = ll + (c - name) + 1;
+            gchar *subname = g_newa(gchar, sl);
+            g_snprintf(subname, sl, "%s%s", locales[i], name);
+            names[i * variants_count + j] = subname;
+        }
+    }
+
+    return _nk_xdg_theme_search_file(self, names, theme_names, NK_XDG_THEME_SOUND_FALLBACK_THEME, _nk_xdg_theme_sound_find_file, profile, _nk_xdg_theme_sound_extensions);
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/bindings.c
@@ -0,0 +1,544 @@
+/*
+ * libnkutils/token - Miscellaneous utilities, token module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <locale.h>
+
+#include <glib.h>
+
+#include <nkutils-bindings.h>
+
+#define MAX_BINDINGS 20
+#define MAX_KEY_SEQUENCE 20
+
+typedef struct {
+    NkBindings *bindings;
+    struct xkb_keymap *keymap;
+    struct xkb_state *master_state;
+    struct xkb_state *state;
+    NkBindingsSeat *seat;
+    guint64 triggered;
+} NkBindingsTestFixture;
+
+typedef struct {
+    const gchar *binding;
+    guint64 trigger;
+} NkBindingsTestBinding;
+
+typedef struct {
+    NkBindingsTestFixture *fixture;
+    guint64 trigger;
+} NkBindingsTestBindingSlice;
+
+typedef struct {
+    xkb_keycode_t key;
+    NkBindingsKeyState state;
+    gboolean fail;
+    const gchar *text;
+    guint64 triggered;
+} NkBindingsTestKey;
+
+typedef struct {
+    NkBindingsTestBinding bindings[MAX_BINDINGS + 1];
+    NkBindingsTestKey keys[MAX_KEY_SEQUENCE + 1];
+} NkBindingsTestData;
+
+/* Using US basic keymap */
+#define KEYCODE_NONE      0
+#define KEYCODE_ESCAPE    9
+#define KEYCODE_Q         24
+#define KEYCODE_CONTROL_L 37
+#define KEYCODE_A         38
+#define KEYCODE_S         39
+#define KEYCODE_D         40
+#define KEYCODE_TILDE     49
+#define KEYCODE_SHIFT_L   50
+#define KEYCODE_ALT_L     64
+#define KEYCODE_SPACE     65
+#define KEYCODE_ALT_GR    108
+#define KEYCODE_SUPER_L   133
+
+#define _NK_BINDINGS_KEYCODE_TO_STRING(s) #s
+#define NK_BINDINGS_KEYCODE_TO_STRING(s) _NK_BINDINGS_KEYCODE_TO_STRING(s)
+
+static const gchar *_nk_bindings_test_key_names[] = {
+    [KEYCODE_ESCAPE] = "Escape",
+    [KEYCODE_Q] = "Q",
+    [KEYCODE_CONTROL_L] = "Control_L",
+    [KEYCODE_A] = "A",
+    [KEYCODE_S] = "S",
+    [KEYCODE_D] = "D",
+    [KEYCODE_TILDE] = "`",
+    [KEYCODE_SHIFT_L] = "Shift_L",
+    [KEYCODE_ALT_L] = "Alt_L",
+    [KEYCODE_SPACE] = "Space",
+    [KEYCODE_ALT_GR] = "AltGr/ISO_Level3_Shift",
+    [KEYCODE_SUPER_L] = "Super_L",
+};
+
+static const struct {
+    const gchar *testpath;
+    NkBindingsTestData data;
+} _nk_bindings_tests_list[] = {
+    {
+        .testpath = "/nkutils/bindings/press/key/found",
+        .data = {
+            .bindings = {
+                { .binding = "s", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_S, .triggered = 1 },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/press/key/not-found/modified",
+        .data = {
+            .bindings = {
+                { .binding = "s", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L },
+                { .key = KEYCODE_S, .text = "\x13" },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/press/combo/minus",
+        .data = {
+            .bindings = {
+                { .binding = "Ctrl-s", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L },
+                { .key = KEYCODE_S, .triggered = 1 },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/press/combo/plus",
+        .data = {
+            .bindings = {
+                { .binding = "Ctrl+s", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L },
+                { .key = KEYCODE_S, .triggered = 1 },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/press/combo/gtk",
+        .data = {
+            .bindings = {
+                { .binding = "<Ctrl>s", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L },
+                { .key = KEYCODE_S, .triggered = 1 },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/press/combo/multi-modifier",
+        .data = {
+            .bindings = {
+                { .binding = "<Ctrl><Alt><Super>S", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L },
+                { .key = KEYCODE_ALT_L },
+                { .key = KEYCODE_SUPER_L },
+                { .key = KEYCODE_SHIFT_L },
+                { .key = KEYCODE_S, .triggered = 1 },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SHIFT_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SUPER_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_ALT_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/press/consume/consumed",
+        .data = {
+            .bindings = {
+                { .binding = "S", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SHIFT_L },
+                { .key = KEYCODE_S, .triggered = 1 },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SHIFT_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/press/consume/not-consumed",
+        .data = {
+            .bindings = {
+                { .binding = "s", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SHIFT_L },
+                { .key = KEYCODE_S, .text = "S" },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SHIFT_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/press/keycode/not-consumed",
+        .data = {
+            .bindings = {
+                { .binding = "<Shift>[" NK_BINDINGS_KEYCODE_TO_STRING(KEYCODE_S) "]", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SHIFT_L },
+                { .key = KEYCODE_S, .triggered = 1 },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SHIFT_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/press/keycode/consumed",
+        .data = {
+            .bindings = {
+                { .binding = "[" NK_BINDINGS_KEYCODE_TO_STRING(KEYCODE_S) "]", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SHIFT_L },
+                { .key = KEYCODE_S, .text = "S" },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SHIFT_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/release/key/exclamation",
+        .data = {
+            .bindings = {
+                { .binding = "!s", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_S },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE, .triggered = 1 },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/release/key/gtk",
+        .data = {
+            .bindings = {
+                { .binding = "<Release>s", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_S },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE, .triggered = 1 },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/release/combo/simple",
+        .data = {
+            .bindings = {
+                { .binding = "<Release><Ctrl>s", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L },
+                { .key = KEYCODE_S },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L, .state = NK_BINDINGS_KEY_STATE_RELEASE, .triggered = 1 },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/release/combo/multi-modifiers",
+        .data = {
+            .bindings = {
+                { .binding = "<Ctrl><Alt><Super>S", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L },
+                { .key = KEYCODE_ALT_L },
+                { .key = KEYCODE_SUPER_L },
+                { .key = KEYCODE_SHIFT_L },
+                { .key = KEYCODE_S, .triggered = 1 },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_CONTROL_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SHIFT_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_SUPER_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_ALT_L, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/press-and-release/key",
+        .data = {
+            .bindings = {
+                { .binding = "s", .trigger = 1 },
+                { .binding = "!s", .trigger = 2 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_A, .text = "a" },
+                { .key = KEYCODE_A, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_S, .triggered = 1 },
+                { .key = KEYCODE_S, .state = NK_BINDINGS_KEY_STATE_RELEASE, .triggered = 2 },
+                { .key = KEYCODE_D, .text = "d" },
+                { .key = KEYCODE_D, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+#ifdef NK_XKBCOMMON_HAS_COMPOSE
+    {
+        .testpath = "/nkutils/bindings/compose/with-binding",
+        .data = {
+            .bindings = {
+                { .binding = "dead_grave", .trigger = 1 },
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_TILDE },
+                { .key = KEYCODE_TILDE, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_TILDE, .text = "`" },
+                { .key = KEYCODE_TILDE, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+    {
+        .testpath = "/nkutils/bindings/compose/without-binding",
+        .data = {
+            .bindings = {
+                { .binding = NULL }
+            },
+            .keys = {
+                { .key = KEYCODE_TILDE },
+                { .key = KEYCODE_TILDE, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_TILDE, .text = "`" },
+                { .key = KEYCODE_TILDE, .state = NK_BINDINGS_KEY_STATE_RELEASE },
+                { .key = KEYCODE_NONE }
+            },
+        }
+    },
+#endif /* NK_XKBCOMMON_HAS_COMPOSE */
+};
+
+static void
+_nk_bindings_tests_setup(NkBindingsTestFixture *fixture, G_GNUC_UNUSED gconstpointer user_data)
+{
+    static const struct xkb_rule_names names = {
+        .layout = "us",
+        .variant = "intl",
+    };
+    fixture->bindings = nk_bindings_new(0);
+    fixture->seat = nk_bindings_seat_new(fixture->bindings, XKB_CONTEXT_NO_ENVIRONMENT_NAMES);
+    fixture->keymap = xkb_keymap_new_from_names(nk_bindings_seat_get_context(fixture->seat), &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
+    fixture->master_state = xkb_state_new(fixture->keymap);
+    fixture->state = xkb_state_new(fixture->keymap);
+    nk_bindings_seat_update_keymap(fixture->seat, fixture->keymap, fixture->state);
+}
+
+static gboolean
+_nk_bindings_tests_callback(guint64 scope, gpointer target, gpointer user_data)
+{
+    NkBindingsTestBindingSlice *slice = user_data;
+    g_assert_cmpuint(scope, ==, 0);
+    g_assert_null(target);
+
+    slice->fixture->triggered = slice->trigger;
+    return TRUE;
+}
+
+static void
+_nk_bindings_tests_func(NkBindingsTestFixture *fixture, gconstpointer user_data)
+{
+    NkBindingsTestData *data = (NkBindingsTestData *) user_data;
+    GError *error = NULL;
+
+    NkBindingsTestBindingSlice slices[MAX_BINDINGS], *slice = slices;
+    NkBindingsTestBinding *binding;
+    for ( binding = data->bindings ; binding->binding != NULL ; ++binding )
+    {
+        gboolean ret;
+
+        slice->fixture = fixture;
+        slice->trigger = binding->trigger;
+        ret = nk_bindings_add_binding(fixture->bindings, 0, binding->binding, _nk_bindings_tests_callback, slice++, NULL, &error);
+
+        g_assert_true(ret);
+        g_assert_no_error(error);
+    }
+
+    NkBindingsTestKey *key;
+    for ( key = data->keys ; key->key != KEYCODE_NONE ; ++key )
+    {
+        gchar *text;
+
+        g_test_message("%s key %s", ( key->state == NK_BINDINGS_KEY_STATE_RELEASE ) ? "Releasing" : ( key->state == NK_BINDINGS_KEY_STATE_PRESSED ) ? "Already pressed" : "Pressing", _nk_bindings_test_key_names[key->key]);
+
+        fixture->triggered = 0;
+        text = nk_bindings_seat_handle_key(fixture->seat, NULL, key->key, key->state);
+
+        if ( xkb_state_update_key(fixture->master_state, key->key, ( key->state == NK_BINDINGS_KEY_STATE_RELEASE ) ? XKB_KEY_UP : XKB_KEY_DOWN) != 0 )
+        {
+            g_test_message("New state is 0x%x", xkb_state_serialize_mods(fixture->master_state, XKB_STATE_MODS_DEPRESSED));
+            nk_bindings_seat_update_mask(fixture->seat, NULL,
+                xkb_state_serialize_mods(fixture->master_state, XKB_STATE_MODS_DEPRESSED),
+                xkb_state_serialize_mods(fixture->master_state, XKB_STATE_MODS_LATCHED),
+                xkb_state_serialize_mods(fixture->master_state, XKB_STATE_MODS_LOCKED),
+                xkb_state_serialize_layout(fixture->master_state, XKB_STATE_LAYOUT_DEPRESSED),
+                xkb_state_serialize_layout(fixture->master_state, XKB_STATE_LAYOUT_LATCHED),
+                xkb_state_serialize_layout(fixture->master_state, XKB_STATE_LAYOUT_LOCKED));
+        }
+
+        g_assert_cmpstr(key->text, ==, text);
+        g_assert_cmpuint(fixture->triggered, ==, key->triggered);
+
+        g_free(text);
+    }
+}
+
+static void
+_nk_bindings_tests_teardown(NkBindingsTestFixture *fixture, G_GNUC_UNUSED gconstpointer user_data)
+{
+    nk_bindings_seat_free(fixture->seat);
+    xkb_state_unref(fixture->state);
+    xkb_keymap_unref(fixture->keymap);
+    nk_bindings_free(fixture->bindings);
+}
+
+int
+main(int argc, char *argv[])
+{
+    setlocale(LC_CTYPE, "en_US.UTF-8");
+    g_test_init(&argc, &argv, NULL);
+
+    g_test_set_nonfatal_assertions();
+
+    gsize i;
+    for ( i = 0 ; i < G_N_ELEMENTS(_nk_bindings_tests_list) ; ++i )
+        g_test_add(_nk_bindings_tests_list[i].testpath, NkBindingsTestFixture, &_nk_bindings_tests_list[i].data, _nk_bindings_tests_setup, _nk_bindings_tests_func, _nk_bindings_tests_teardown);
+    return g_test_run();
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/colour.c
@@ -0,0 +1,299 @@
+/*
+ * libnkutils/colour - Miscellaneous utilities, colour module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <string.h>
+
+#include <glib.h>
+
+#include <nkutils-colour.h>
+
+
+#define g_assert_cmpfloat_near(a, b, delta) G_STMT_START { g_assert_cmpfloat((b - delta), <, a); g_assert_cmpfloat(a, <, (b + delta)); } G_STMT_END
+
+typedef struct {
+    const gchar *string;
+    const gchar *generated_string;
+    const NkColour colour;
+    gboolean ret;
+} NkColourTestData;
+
+static const struct {
+    const gchar *testpath;
+    NkColourTestData data;
+} _nk_colour_tests_list[] = {
+    {
+        .testpath = "/nkutils/colour/null",
+        .data ={
+            .string = NULL,
+            .ret = FALSE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/hex/8",
+        .data ={
+            .string = "#ffddee7f",
+            .colour = {
+                .red   = 1.,
+                .green = .866,
+                .blue  = .933,
+                .alpha = .498
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/hex/6",
+        .data ={
+            .string = "#121314",
+            .colour = {
+                .red   = .070,
+                .green = .074,
+                .blue  = .078,
+                .alpha = 1.
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/hex/4",
+        .data ={
+            .string = "#abcd",
+            .generated_string = "#aabbccdd",
+            .colour = {
+                .red   = .666,
+                .green = .733,
+                .blue  = .800,
+                .alpha = .866
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/hex/3",
+        .data ={
+            .string = "#369",
+            .generated_string = "#336699",
+            .colour = {
+                .red   = .200,
+                .green = .400,
+                .blue  = .600,
+                .alpha = 1.
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/hex/bad",
+        .data ={
+            .string = "#69",
+            .ret = FALSE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/rgb",
+        .data ={
+            .string = "rgb(51, 102, 153)",
+            .generated_string = "rgb(51.0000000000,102.0000000000,153.0000000000)",
+            .colour = {
+                .red   = .200,
+                .green = .400,
+                .blue  = .600,
+                .alpha = 1.
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/rgb/percentage",
+        .data ={
+            .string = "rgb(100%, 50%, 0%)",
+            .generated_string = "rgb(255.0000000000,127.5000000000,0.0000000000)",
+            .colour = {
+                .red   = 1.,
+                .green = .500,
+                .blue  = 0.,
+                .alpha = 1.
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/rgba",
+        .data ={
+            .string = "rgba(255, 0, 127, .1)",
+            .generated_string = "rgba(255.0000000000,0.0000000000,126.9900000000,0.1000000000)",
+            .colour = {
+                .red   = 1.,
+                .green = 0.,
+                .blue  = .498,
+                .alpha = .1
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/rgb/bad/1",
+        .data ={
+            .string = "rgb(100%, 50%, 0%",
+            .ret = FALSE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/rgb/bad/2",
+        .data ={
+            .string = "rgb100%, 50%, 0%",
+            .ret = FALSE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/hsl/plain",
+        .data ={
+            .string = "hsl(210, 50%, 50%)",
+            .generated_string = "#4080bf",
+            .colour = {
+                .red   = .25,
+                .green = .50,
+                .blue  = .75,
+                .alpha = 1.
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/hsl/alpha",
+        .data ={
+            .string = "hsla(210, 50%, 50%, 0.75)",
+            .generated_string = "#4080bfbf",
+            .colour = {
+                .red   = .25,
+                .green = .50,
+                .blue  = .75,
+                .alpha = .75
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/hwb/plain",
+        .data ={
+            .string = "hwb(120, 20%, 60%)",
+            .generated_string = "#336633",
+            .colour = {
+                .red   = .2,
+                .green = .4,
+                .blue  = .2,
+                .alpha = 1.
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/hwb/alpha",
+        .data ={
+            .string = "hwb(120, 20%, 60%, 0.5)",
+            .generated_string = "#33663380",
+            .colour = {
+                .red   = .2,
+                .green = .4,
+                .blue  = .2,
+                .alpha = .5
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/named",
+        .data ={
+            .string = "gold",
+            .generated_string = "#ffd700",
+            .colour = {
+                .red   = 1.,
+                .green = .843,
+                .blue  = 0.,
+                .alpha = 1.
+            },
+            .ret = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/colour/bad",
+        .data ={
+            .string = "no-colour-has-that-name",
+            .ret = FALSE,
+        }
+    },
+};
+
+static void
+_nk_colour_tests_func(gconstpointer user_data)
+{
+    const NkColourTestData *data = user_data;
+
+    NkColour colour = { .alpha = 0 };
+    gboolean r;
+    r = nk_colour_parse(data->string, &colour);
+    if ( data->ret )
+        g_assert_true(r);
+    else
+        g_assert_false(r);
+    g_assert_cmpfloat_near(colour.red, data->colour.red, 0.001);
+    g_assert_cmpfloat_near(colour.green, data->colour.green, 0.001);
+    g_assert_cmpfloat_near(colour.blue, data->colour.blue, 0.001);
+    g_assert_cmpfloat_near(colour.alpha, data->colour.alpha, 0.001);
+
+    if ( ! r )
+        return;
+
+    const gchar *string;
+    const gchar *wanted_string = ( data->generated_string != NULL ) ? data->generated_string : data->string;
+
+    if ( g_str_has_prefix(wanted_string, "rgb") )
+        string = nk_colour_to_rgba(&data->colour);
+    else
+        string = nk_colour_to_hex(&data->colour);
+
+    g_assert_nonnull(string);
+    g_assert_cmpstr(string, ==, wanted_string);
+}
+
+
+
+int
+main(int argc, char *argv[])
+{
+    g_test_init(&argc, &argv, NULL);
+
+    g_test_set_nonfatal_assertions();
+
+    gsize i;
+    for ( i = 0 ; i < G_N_ELEMENTS(_nk_colour_tests_list) ; ++i )
+        g_test_add_data_func(_nk_colour_tests_list[i].testpath, &_nk_colour_tests_list[i].data, _nk_colour_tests_func);
+
+    return g_test_run();
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/enum.c
@@ -0,0 +1,149 @@
+/*
+ * libnkutils/enum - Miscellaneous utilities, enum module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <glib.h>
+
+#include <nkutils-enum.h>
+
+typedef enum {
+    CENTER,
+    LEFT,
+    RIGHT,
+    _MAX_VALUES
+} NkEnumTestValues;
+
+static const gchar * const _nk_enum_tests_values[_MAX_VALUES] = {
+    [CENTER] = "center",
+    [LEFT] = "left",
+    [RIGHT] = "right",
+};
+
+typedef struct {
+    const gchar *string;
+    gboolean ignore_case;
+    gboolean prefix;
+    gboolean ret;
+    NkEnumTestValues value;
+    const gchar * const values[_MAX_VALUES];
+} NkEnumTestData;
+
+static const struct {
+    const gchar *testpath;
+    NkEnumTestData data;
+} _nk_enum_tests_list[] = {
+    {
+        .testpath = "/nkutils/enum/full/exists",
+        .data = {
+            .string = "center",
+            .ignore_case = FALSE,
+            .prefix = FALSE,
+            .ret = TRUE,
+            .value = CENTER,
+        }
+    },
+    {
+        .testpath = "/nkutils/enum/full/missing",
+        .data = {
+            .string = "Center",
+            .ignore_case = FALSE,
+            .prefix = FALSE,
+            .ret = FALSE,
+            .value = _MAX_VALUES,
+        }
+    },
+    {
+        .testpath = "/nkutils/enum/full/case",
+        .data = {
+            .string = "Center",
+            .ignore_case = TRUE,
+            .prefix = FALSE,
+            .ret = TRUE,
+            .value = CENTER,
+        }
+    },
+    {
+        .testpath = "/nkutils/enum/prefix/full",
+        .data = {
+            .string = "center",
+            .ignore_case = FALSE,
+            .prefix = TRUE,
+            .ret = TRUE,
+            .value = CENTER,
+        }
+    },
+    {
+        .testpath = "/nkutils/enum/prefix/prefix",
+        .data = {
+            .string = "cen",
+            .ignore_case = FALSE,
+            .prefix = TRUE,
+            .ret = FALSE,
+            .value = _MAX_VALUES,
+        }
+    },
+    {
+        .testpath = "/nkutils/enum/prefix/no-prefix",
+        .data = {
+            .string = "cen",
+            .ignore_case = FALSE,
+            .prefix = FALSE,
+            .ret = FALSE,
+            .value = _MAX_VALUES,
+        }
+    },
+};
+
+static void
+_nk_enum_tests_func(gconstpointer user_data)
+{
+    const NkEnumTestData *data = user_data;
+
+    guint64 value = _MAX_VALUES;
+    gboolean r;
+    r = nk_enum_parse(data->string, _nk_enum_tests_values, _MAX_VALUES, data->ignore_case, data->prefix, &value);
+    if ( data->ret )
+        g_assert_true(r);
+    else
+        g_assert_false(r);
+    g_assert_cmpuint(data->value, ==, value);
+}
+
+int
+main(int argc, char *argv[])
+{
+    g_test_init(&argc, &argv, NULL);
+
+    g_test_set_nonfatal_assertions();
+
+    gsize i;
+    for ( i = 0 ; i < G_N_ELEMENTS(_nk_enum_tests_list) ; ++i )
+        g_test_add_data_func(_nk_enum_tests_list[i].testpath, &_nk_enum_tests_list[i].data, _nk_enum_tests_func);
+
+    return g_test_run();
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/gtk-3.0/settings.ini
@@ -0,0 +1,3 @@
+[Settings]
+gtk-double-click-time = 300
+gtk-cursor-theme-name = gnome
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/gtk-4.0/settings.ini
@@ -0,0 +1,4 @@
+[Settings]
+gtk-double-click-time = 300
+gtk-icon-theme-name = nothing-like-this-theme
+gtk-enable-primary-paste = true
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/gtk-settings.c
@@ -0,0 +1,149 @@
+/*
+ * libnkutils/xdg-de - Miscellaneous utilities, XDG DE module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <locale.h>
+
+#include <glib.h>
+
+#include <nkutils-gtk-settings.h>
+
+typedef struct {
+    const gchar *key;
+    GOptionArg type;
+    union {
+        gboolean boolean;
+        gint64 int64;
+        const gchar *string;
+    } value;
+} NkGtkSettingsTestData;
+
+static const struct {
+    const gchar *testpath;
+    NkGtkSettingsTestData data;
+} _nk_gtk_settings_tests_list[] = {
+    {
+        .testpath = "/nkutils/gtk-settings/4.0/boolean",
+        .data = {
+            .key = "gtk-enable-primary-paste",
+            .type = G_OPTION_ARG_NONE,
+            .value.boolean = TRUE,
+        }
+    },
+    {
+        .testpath = "/nkutils/gtk-settings/4.0/uint64",
+        .data = {
+            .key = "gtk-double-click-time",
+            .type = G_OPTION_ARG_INT64,
+            .value.int64 = 300,
+        }
+    },
+    {
+        .testpath = "/nkutils/gtk-settings/4.0/string",
+        .data = {
+            .key = "gtk-icon-theme-name",
+            .type = G_OPTION_ARG_STRING,
+            .value.string = "nothing-like-this-theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/gtk-settings/3.0/string",
+        .data = {
+            .key = "gtk-cursor-theme-name",
+            .type = G_OPTION_ARG_STRING,
+            .value.string = "gnome",
+        }
+    },
+};
+
+static void
+_nk_gtk_settings_tests_func(gconstpointer user_data)
+{
+    const NkGtkSettingsTestData *data = user_data;
+    const gchar *keys[NK_GTK_SETTINGS_NUM_VERSION] = {
+        data->key,
+        data->key,
+    };
+
+    switch ( data->type )
+    {
+    case G_OPTION_ARG_NONE:
+    {
+        gboolean ret;
+        gboolean value;
+        ret = nk_gtk_settings_get_boolean(&value, keys);
+        g_assert_true(ret);
+        if ( data->value.boolean )
+            g_assert_true(value);
+        else
+            g_assert_false(value);
+    }
+    break;
+    case G_OPTION_ARG_INT64:
+    {
+        gboolean ret;
+        guint64 value;
+        ret = nk_gtk_settings_get_uint64(&value, keys);
+        g_assert_true(ret);
+        g_assert_cmpuint(value, ==, data->value.int64);
+    }
+    break;
+    case G_OPTION_ARG_STRING:
+    {
+        gboolean ret;
+        gchar *value;
+        ret = nk_gtk_settings_get_string(&value, keys);
+        g_assert_true(ret);
+        g_assert_cmpstr(value, ==, data->value.string);
+        g_free(value);
+    }
+    break;
+    default:
+        g_assert_not_reached();
+    }
+}
+
+int
+main(int argc, char *argv[])
+{
+    setlocale(LC_ALL, "");
+
+    g_test_init(&argc, &argv, NULL);
+
+    g_test_set_nonfatal_assertions();
+
+    g_setenv("XDG_CONFIG_HOME", SRCDIR G_DIR_SEPARATOR_S "tests", TRUE);
+    g_setenv("XDG_DATA_DIRS", "/not/existing/dir", TRUE);
+
+    gsize i;
+    for ( i = 0 ; i < G_N_ELEMENTS(_nk_gtk_settings_tests_list) ; ++i )
+        g_test_add_data_func(_nk_gtk_settings_tests_list[i].testpath, &_nk_gtk_settings_tests_list[i].data, _nk_gtk_settings_tests_func);
+
+    int ret = g_test_run();
+    return ret;
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/icons/recursive-theme-test/index.theme
@@ -0,0 +1,10 @@
+[Icon Theme]
+Name=recursive-theme-test
+Inherits=recursive-theme-test
+Comment=The Only One
+Example=test-icon
+Directories=test-dir
+[test-dir]
+Context=Emblems
+Size=10
+Type=fixed
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/token.c
@@ -0,0 +1,731 @@
+/*
+ * libnkutils/token - Miscellaneous utilities, token module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <string.h>
+
+#include <glib.h>
+
+#include <nkutils-token.h>
+
+#define MAX_DATA 4
+
+typedef struct {
+    const gchar *token;
+    const gchar *key;
+    gint64 index;
+    const gchar *content;
+} NkTokenTestDataData;
+
+typedef struct {
+    gunichar identifier;
+    const gchar *source;
+    NkTokenTestDataData data[MAX_DATA + 1];
+    gint error;
+    const gchar *result;
+} NkTokenTestData;
+
+static const struct {
+    const gchar *testpath;
+    NkTokenTestData data;
+} _nk_token_list_tests_list[] = {
+    {
+        .testpath = "/nkutils/token/basic",
+        .data = {
+            .identifier = '$',
+            .source = "You can make ${recipe} with ${fruit}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "a banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana split with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/basic/dash-underscore",
+        .data = {
+            .identifier = '$',
+            .source = "You can make ${recipe_name} with ${fruit-name}.",
+            .data = {
+                { .token = "fruit-name", .content = "a banana" },
+                { .token = "recipe_name", .content = "a banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana split with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/basic/unicode",
+        .data = {
+            .identifier = '$',
+            .source = "You can make ${recette} with ${ingrédient}.",
+            .data = {
+                { .token = "ingrédient", .content = "a banana" },
+                { .token = "recette", .content = "a banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana split with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/basic/wrong/1",
+        .data = {
+            .identifier = '$',
+            .source = "You can make ${recipe} with $fruit.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "a banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana split with $fruit."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/basic/wrong/2",
+        .data = {
+            .identifier = '$',
+            .source = "$fruit is good.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "a banana split" },
+                { .token = NULL }
+            },
+            .result = "$fruit is good."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/key/index",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe[0]} with ${fruit}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .key = "", .index = 0, .content = "banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana split with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/key/name",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe[icecream]} with ${fruit}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .key = "icecream", .content = "banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana split with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/key/name/modifier",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe[cake]:-banana cake} with ${fruit}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .key = "cream", .content = "banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana cake with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/key/join/replace",
+        .data = {
+            .identifier = '$',
+            .source = "You can make [${recipes[@@]/@/], [}] with ${fruit}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipes", .key = "@@", .content = "banana pie@banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make [banana pie], [banana split] with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/wrong/modifier",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe} with ${fruit::}.",
+            .error = NK_TOKEN_ERROR_UNKNOWN_MODIFIER,
+        }
+    },
+    {
+        .testpath = "/nkutils/token/wrong/key/index",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe[18446744073709551616]} with ${fruit}.",
+            .error = NK_TOKEN_ERROR_WRONG_KEY,
+        }
+    },
+    {
+        .testpath = "/nkutils/token/wrong/key",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe[|]} with ${fruit}.",
+            .error = NK_TOKEN_ERROR_WRONG_KEY,
+        }
+    },
+    {
+        .testpath = "/nkutils/token/wrong/regex/pattern",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe/[} with ${fruit}.",
+            .error = NK_TOKEN_ERROR_REGEX,
+        }
+    },
+    {
+        .testpath = "/nkutils/token/wrong/regex/replace",
+        .data = {
+            .identifier = '$',
+            .source = "You can make ${recipe/a/\\gwrong} with ${fruit}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "a banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make  with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/fallback/with",
+        .data = {
+            .identifier = '$',
+            .source = "I want to eat ${fruit:-an apple}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = NULL }
+            },
+            .result = "I want to eat a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/fallback/without",
+        .data = {
+            .identifier = '$',
+            .source = "I want to eat ${fruit:-an apple}.",
+            .data = {
+                { .token = NULL }
+            },
+            .result = "I want to eat an apple."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/fallback/recurse",
+        .data = {
+            .identifier = '$',
+            .source = "I want to eat ${fruit:-${vegetable}}.",
+            .data = {
+                { .token = "vegetable", .content = "a zucchini" },
+                { .token = NULL }
+            },
+            .result = "I want to eat a zucchini."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/substitute/with",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${adjective:+(}${adjective}${adjective:+) }${recipe} with ${fruit}${addition:+ and }${addition}.",
+            .data = {
+                { .token = "adjective", .content = "creamy" },
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = "addition", .content = "some cream" },
+                { .token = NULL }
+            },
+            .result = "You can make a (creamy) banana split with a banana and some cream."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/substitute/without",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${adjective:+(}${adjective}${adjective:+) }${recipe} with ${fruit}${addition:+ and }${addition}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana split with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/anti-substitute/with",
+        .data = {
+            .identifier = '$',
+            .source = "I want to eat a ${adjective:!sweat }lemon.",
+            .data = {
+                { .token = "adjective", .content = "juicy" },
+                { .token = NULL }
+            },
+            .result = "I want to eat a lemon."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/anti-substitute/without",
+        .data = {
+            .identifier = '$',
+            .source = "I want to eat a ${adjective:!sweat }lemon.",
+            .data = {
+                { .token = NULL }
+            },
+            .result = "I want to eat a sweat lemon."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/full",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe/split/cream} with ${fruit}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana cream with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/missing",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe} with ${fruit}${addition/^/ and }.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana split with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/capture",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${adjective/(.+)/(\\1) }${recipe} with ${fruit}${addition/^/ and }.",
+            .data = {
+                { .token = "adjective", .content = "creamy" },
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = "addition", .content = "some cream" },
+                { .token = NULL }
+            },
+            .result = "You can make a (creamy) banana split with a banana and some cream."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/before-after/with",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${adjective/^/(/$/) }${recipe} with ${fruit}${addition/^/ and }.",
+            .data = {
+                { .token = "adjective", .content = "creamy" },
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = "addition", .content = "some cream" },
+                { .token = NULL }
+            },
+            .result = "You can make a (creamy) banana split with a banana and some cream."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/before-after/without",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${adjective/^/(/$/) }${recipe} with ${fruit}${addition/^/ and }.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana split with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/remove",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe/ split} with ${fruit}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a banana with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/multiple",
+        .data = {
+            .identifier = '$',
+            .source = "You can make ${recipe/a banana/an apple pie/ split} with ${fruit/.+/apples}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "a banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make an apple pie with apples."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/braces/paired",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${adjective/.{2}$/y/^/(/$/) }${recipe} with ${fruit}${addition/\\{//\\}//^/ and }.",
+            .data = {
+                { .token = "adjective", .content = "creamed" },
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = "addition", .content = "some cream{}" },
+                { .token = NULL }
+            },
+            .result = "You can make a (creamy) banana split with a banana and some cream."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/braces/opening",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${adjective/.{2}$/y/^/(/$/) }${recipe} with ${fruit}${addition/\\{//^/ and }.",
+            .data = {
+                { .token = "adjective", .content = "creamed" },
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = "addition", .content = "some cream{" },
+                { .token = NULL }
+            },
+            .result = "You can make a (creamy) banana split with a banana and some cream."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/braces/closing",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${adjective/.{2}$/y/^/(/$/) }${recipe} with ${fruit}${addition/\\}//^/ and }.",
+            .data = {
+                { .token = "adjective", .content = "creamed" },
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = "addition", .content = "some cream}" },
+                { .token = NULL }
+            },
+            .result = "You can make a (creamy) banana split with a banana and some cream."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/escaping/backslash",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${adjective/^/(/$/) /\\\\}${recipe} with ${fruit}${addition/^/ and }.",
+            .data = {
+                { .token = "adjective", .content = "creamy\\" },
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = "addition", .content = "some cream" },
+                { .token = NULL }
+            },
+            .result = "You can make a (creamy) banana split with a banana and some cream."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/escaping/forwardslash/1",
+        .data = {
+            .identifier = '$',
+            .source = "${data/\\/}",
+            .data = {
+                { .token = "data", .content = "/" },
+                { .token = NULL }
+            },
+            .result = ""
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/escaping/forwardslash/2",
+        .data = {
+            .identifier = '$',
+            .source = "${data/a/\\/}",
+            .data = {
+                { .token = "data", .content = "a" },
+                { .token = NULL }
+            },
+            .result = "/"
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/escaping/forwardslash/3",
+        .data = {
+            .identifier = '$',
+            .source = "${data/a/\\//b/x}",
+            .data = {
+                { .token = "data", .content = "ab" },
+                { .token = NULL }
+            },
+            .result = "/x"
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/escaping/right-curly-bracket/2",
+        .data = {
+            .identifier = '$',
+            .source = "${data/a/\\}}",
+            .data = {
+                { .token = "data", .content = "a" },
+                { .token = NULL }
+            },
+            .result = "}"
+        }
+    },
+    {
+        .testpath = "/nkutils/token/replace/recurse/with",
+        .data = {
+            .identifier = '$',
+            .source = "I want to eat ${recipe/an apple/${fruit}}.",
+            .data = {
+                { .token = "recipe", .content = "an apple pie" },
+                { .token = "fruit", .content = "a blackberry" },
+                { .token = NULL }
+            },
+            .result = "I want to eat a blackberry pie."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/old/before-after",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${(<adjective>) }${recipe} with ${fruit}${ and <addition}.",
+            .data = {
+                { .token = "fruit", .content = "a banana" },
+                { .token = "recipe", .content = "banana split" },
+                { .token = NULL }
+            },
+            .result = "You can make a ${(<adjective>) }banana split with a banana${ and <addition}."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/identifier/double-escape",
+        .data = {
+            .identifier = '$',
+            .source = "echo $${PATH}",
+            .data = {
+                { .token = NULL }
+            },
+            .result = "echo ${PATH}"
+        }
+    },
+    {
+        .testpath = "/nkutils/token/identifier/non-dollar",
+        .data = {
+            .identifier = '%',
+            .source = "Some %{variable}",
+            .data = {
+                { .token = "variable", .content = "value" },
+                { .token = NULL }
+            },
+            .result = "Some value"
+        }
+    },
+    {
+        .testpath = "/nkutils/token/identifier/none",
+        .data = {
+            .identifier = '\0',
+            .source = "Some {variable}",
+            .data = {
+                { .token = "variable", .content = "value" },
+                { .token = NULL }
+            },
+            .result = "Some value"
+        }
+    },
+};
+
+static const gchar *
+_nk_token_list_tests_callback(const gchar *token, guint64 value, const gchar *key, gint64 index, gpointer user_data)
+{
+    NkTokenTestData *test_data = user_data;
+    NkTokenTestDataData *data;
+    g_assert_cmpuint(value, ==, 0);
+    for ( data = test_data->data ; data->token != NULL ; ++data )
+    {
+        if ( ( g_strcmp0(token, data->token) == 0 ) && ( g_strcmp0(key, data->key) == 0 ) && ( index == data->index ) )
+            return data->content;
+    }
+    return NULL;
+}
+
+static void
+_nk_token_list_tests_func(gconstpointer user_data)
+{
+    NkTokenTestData *data = (NkTokenTestData *) user_data;
+    NkTokenList *token_list;
+    GError *error = NULL;
+
+    token_list = nk_token_list_parse(g_strdup(data->source), data->identifier, &error);
+    if ( data->result == NULL )
+    {
+        g_assert_null(token_list);
+        g_assert_error(error, NK_TOKEN_ERROR, data->error);
+        return;
+    }
+    g_assert_nonnull(token_list);
+    g_assert_no_error(error);
+    g_assert_nonnull(nk_token_list_ref(token_list));
+
+    gchar *result;
+    result = nk_token_list_replace(token_list, _nk_token_list_tests_callback, data);
+
+    g_assert_cmpstr(result, ==, data->result);
+
+    nk_token_list_unref(token_list);
+    nk_token_list_unref(token_list);
+}
+
+typedef enum {
+    TOKEN_FRUIT,
+    TOKEN_RECIPE,
+    _TOKEN_SIZE
+} NkTokenListEnumTokens;
+
+static const gchar * const _nk_token_list_enum_tests_tokens[_TOKEN_SIZE] = {
+    [TOKEN_FRUIT]  = "fruit",
+    [TOKEN_RECIPE] = "recipe",
+};
+
+typedef struct {
+    gunichar identifier;
+    const gchar *source;
+    gchar *data[_TOKEN_SIZE];
+    guint64 used_tokens;
+    gint error;
+    const gchar *result;
+} NkTokenListEnumTestData;
+
+static const struct {
+    const gchar *testpath;
+    NkTokenListEnumTestData data;
+} _nk_token_list_enum_tests_list[] = {
+    {
+        .testpath = "/nkutils/token/enum/basic",
+        .data = {
+            .identifier = '$',
+            .source = "You can make ${recipe} with ${fruit}.",
+            .data = {
+                [TOKEN_FRUIT]  = "a banana",
+                [TOKEN_RECIPE] = "a banana split",
+            },
+            .used_tokens = (1 << TOKEN_FRUIT) | (1 << TOKEN_RECIPE),
+            .result = "You can make a banana split with a banana."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/enum/nested",
+        .data = {
+            .identifier = '$',
+            .source = "I want to eat ${recipe:+${fruit} ${recipe}}.",
+            .data = {
+                [TOKEN_FRUIT]  = "an apple",
+                [TOKEN_RECIPE] = "pie",
+            },
+            .used_tokens = (1 << TOKEN_FRUIT) | (1 << TOKEN_RECIPE),
+            .result = "I want to eat an apple pie."
+        }
+    },
+    {
+        .testpath = "/nkutils/token/enum/wrong/regex",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe/[} with ${fruit}.",
+            .error = NK_TOKEN_ERROR_REGEX,
+        }
+    },
+    {
+        .testpath = "/nkutils/token/enum/wrong/token",
+        .data = {
+            .identifier = '$',
+            .source = "You can make a ${recipe} with ${fruit} and ${addition}.",
+            .error = NK_TOKEN_ERROR_UNKNOWN_TOKEN,
+        }
+    },
+};
+
+static const gchar *
+_nk_token_list_enum_tests_callback(const gchar *token, guint64 value, G_GNUC_UNUSED const gchar *key, G_GNUC_UNUSED  gint64 index, gpointer user_data)
+{
+    const gchar * const *data = user_data;
+    g_assert_cmpstr(token, ==, _nk_token_list_enum_tests_tokens[value]);
+    return data[value];
+}
+
+static void
+_nk_token_list_enum_tests_func(gconstpointer user_data)
+{
+    NkTokenListEnumTestData *data = (NkTokenListEnumTestData *) user_data;
+    NkTokenList *token_list;
+    guint64 used_tokens;
+    GError *error = NULL;
+
+    token_list = nk_token_list_parse_enum(g_strdup(data->source), data->identifier, _nk_token_list_enum_tests_tokens, _TOKEN_SIZE, &used_tokens, &error);
+    if ( data->result == NULL )
+    {
+        g_assert_null(token_list);
+        g_assert_error(error, NK_TOKEN_ERROR, data->error);
+        return;
+    }
+    g_assert_no_error(error);
+    g_assert_nonnull(token_list);
+    if ( data->used_tokens != 0 )
+        g_assert_cmpuint(used_tokens, ==, data->used_tokens);
+
+    gchar *result;
+    result = nk_token_list_replace(token_list, _nk_token_list_enum_tests_callback, data->data);
+
+    g_assert_cmpstr(result, ==, data->result);
+
+    nk_token_list_unref(token_list);
+}
+
+int
+main(int argc, char *argv[])
+{
+    g_test_init(&argc, &argv, NULL);
+
+    g_test_set_nonfatal_assertions();
+
+    gsize i;
+    for ( i = 0 ; i < G_N_ELEMENTS(_nk_token_list_tests_list) ; ++i )
+        g_test_add_data_func(_nk_token_list_tests_list[i].testpath, &_nk_token_list_tests_list[i].data, _nk_token_list_tests_func);
+
+    for ( i = 0 ; i < G_N_ELEMENTS(_nk_token_list_enum_tests_list) ; ++i )
+        g_test_add_data_func(_nk_token_list_enum_tests_list[i].testpath, &_nk_token_list_enum_tests_list[i].data, _nk_token_list_enum_tests_func);
+
+    return g_test_run();
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/uuid.c
@@ -0,0 +1,112 @@
+/*
+ * libnkutils/token - Miscellaneous utilities, token module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <string.h>
+
+#include <glib.h>
+
+#include <nkutils-uuid.h>
+
+typedef struct {
+    const gchar *ns;
+    const gchar *name;
+    const gchar *result;
+} NkUuidTestData;
+
+static const struct {
+    const gchar *testpath;
+    NkUuidTestData data;
+} _nk_uuid_tests_list[] = {
+    {
+        .testpath = "/nkutils/uuid/v5/1",
+        .data = {
+            .ns = "02ec50af-981d-4ec3-b520-d395abe89653",
+            .name = "test-domain.example",
+            /* The hash result conflicts with version */
+            .result = "625fe677-3039-521b-a02c-aeb990651e07"
+        }
+    },
+    {
+        .testpath = "/nkutils/uuid/v5/2",
+        .data = {
+            .ns = "c14acc86-fecd-435c-8fc8-09b14ac22f5b",
+            .name = "example.org",
+            /* The hash result conflicts with both variant and version */
+            .result = "6c238dbe-3010-551e-89c3-8ef98d6f4d1b"
+        }
+    },
+};
+
+static void
+_nk_uuid_tests_parse_func(void)
+{
+    NkUuid uuid = NK_UUID_INIT, uuid2 = NK_UUID_INIT;
+
+    nk_uuid_generate(&uuid);
+    g_assert_true(nk_uuid_parse(&uuid2, uuid.string));
+    g_assert_cmpstr(uuid.string, ==, uuid2.string);
+}
+
+static void
+_nk_uuid_tests_fail_func(gconstpointer user_data)
+{
+    const gchar *str = user_data;
+    NkUuid uuid = NK_UUID_INIT;
+
+    g_assert_false(nk_uuid_parse(&uuid, str));
+}
+
+static void
+_nk_uuid_tests_ns_func(gconstpointer user_data)
+{
+    const NkUuidTestData *data = user_data;
+
+    NkUuid uuid = NK_UUID_INIT;
+    g_assert_true(nk_uuid_parse(&uuid, data->ns));
+    g_assert_cmpstr(uuid.string, ==, data->ns);
+    nk_uuid_from_name(&uuid, data->name, -1);
+    g_assert_cmpstr(uuid.string, ==, data->result);
+}
+
+int
+main(int argc, char *argv[])
+{
+    g_test_init(&argc, &argv, NULL);
+
+    g_test_set_nonfatal_assertions();
+
+    g_test_add_func("/nkutils/uuui/generation", _nk_uuid_tests_parse_func);
+    g_test_add_data_func("/nkutils/uuui/parse/fail", "z0c246e98-6678-49b1-bf28-82f383012e86", _nk_uuid_tests_fail_func);
+
+    gsize i;
+    for ( i = 0 ; i < G_N_ELEMENTS(_nk_uuid_tests_list) ; ++i )
+        g_test_add_data_func(_nk_uuid_tests_list[i].testpath, &_nk_uuid_tests_list[i].data, _nk_uuid_tests_ns_func);
+
+    return g_test_run();
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/xdg-de.c
@@ -0,0 +1,238 @@
+/*
+ * libnkutils/xdg-de - Miscellaneous utilities, XDG DE module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <locale.h>
+
+#include <glib.h>
+
+#include <nkutils-xdg-de.h>
+
+typedef struct {
+    const gchar *var;
+    const gchar *value;
+    NkXdgDe de;
+} NkXdgDeTestData;
+
+static const struct {
+    const gchar *testpath;
+    NkXdgDeTestData data;
+} _nk_xdg_de_tests_list[] = {
+    {
+        .testpath = "/nkutils/xdg-de/XDG_CURRENT_DESKTOP/generic",
+        .data = {
+            .var = "XDG_CURRENT_DESKTOP",
+            .value = "X-Generic",
+            .de = NK_XDG_DE_NONE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_CURRENT_DESKTOP/gnome/case-match",
+        .data = {
+            .var = "XDG_CURRENT_DESKTOP",
+            .value = "GNOME",
+            .de = NK_XDG_DE_GNOME,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_CURRENT_DESKTOP/gnome/multi-names",
+        .data = {
+            .var = "XDG_CURRENT_DESKTOP",
+            .value = "GNOME:GNOME-Flashback",
+            .de = NK_XDG_DE_GNOME,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_CURRENT_DESKTOP/gnome/case-not-match",
+        .data = {
+            .var = "XDG_CURRENT_DESKTOP",
+            .value = "gnome",
+            .de = NK_XDG_DE_NONE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_CURRENT_DESKTOP/kde",
+        .data = {
+            .var = "XDG_CURRENT_DESKTOP",
+            .value = "KDE",
+            .de = NK_XDG_DE_KDE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_CURRENT_DESKTOP/unknown",
+        .data = {
+            .var = "XDG_CURRENT_DESKTOP",
+            .value = "WeirdDE",
+            .de = NK_XDG_DE_NONE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_SESSION_DESKTOP/generic",
+        .data = {
+            .var = "XDG_SESSION_DESKTOP",
+            .value = "generic",
+            .de = NK_XDG_DE_NONE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_SESSION_DESKTOP/gnome/case-match",
+        .data = {
+            .var = "XDG_SESSION_DESKTOP",
+            .value = "GNOME",
+            .de = NK_XDG_DE_GNOME,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_SESSION_DESKTOP/gnome/case-not-match",
+        .data = {
+            .var = "XDG_SESSION_DESKTOP",
+            .value = "gnome",
+            .de = NK_XDG_DE_GNOME,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_SESSION_DESKTOP/gnome/flashback",
+        .data = {
+            .var = "XDG_SESSION_DESKTOP",
+            .value = "GNOME-Flashback",
+            .de = NK_XDG_DE_GNOME,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_SESSION_DESKTOP/kde",
+        .data = {
+            .var = "XDG_SESSION_DESKTOP",
+            .value = "KDE",
+            .de = NK_XDG_DE_KDE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/XDG_SESSION_DESKTOP/unknown",
+        .data = {
+            .var = "XDG_SESSION_DESKTOP",
+            .value = "WeirdDE",
+            .de = NK_XDG_DE_NONE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/GNOME_DESKTOP_SESSION_ID",
+        .data = {
+            .var = "GNOME_DESKTOP_SESSION_ID",
+            .value = "1",
+            .de = NK_XDG_DE_GNOME,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/KDE_FULL_SESSION",
+        .data = {
+            .var = "KDE_FULL_SESSION",
+            .value = "1",
+            .de = NK_XDG_DE_KDE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/DESKTOP_SESSION/generic",
+        .data = {
+            .var = "DESKTOP_SESSION",
+            .value = "generic",
+            .de = NK_XDG_DE_NONE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/DESKTOP_SESSION/gnome/case-match",
+        .data = {
+            .var = "DESKTOP_SESSION",
+            .value = "gnome",
+            .de = NK_XDG_DE_GNOME,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/DESKTOP_SESSION/gnome/case-not-match",
+        .data = {
+            .var = "DESKTOP_SESSION",
+            .value = "GNOME",
+            .de = NK_XDG_DE_NONE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/DESKTOP_SESSION/kde",
+        .data = {
+            .var = "DESKTOP_SESSION",
+            .value = "kde",
+            .de = NK_XDG_DE_KDE,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-de/DESKTOP_SESSION/unkown",
+        .data = {
+            .var = "DESKTOP_SESSION",
+            .value = "weirdde",
+            .de = NK_XDG_DE_NONE,
+        }
+    },
+};
+
+static void
+_nk_xdg_de_tests_func(gconstpointer user_data)
+{
+    const NkXdgDeTestData *data = user_data;
+
+    if ( g_test_subprocess() )
+    {
+        NkXdgDe de;
+        g_setenv(data->var, data->value, TRUE);
+        de = nk_xdg_de_detect();
+        g_assert_cmpint(de, ==, data->de);
+        return;
+    }
+    g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_INHERIT_STDOUT | G_TEST_SUBPROCESS_INHERIT_STDERR);
+    g_test_trap_assert_passed();
+}
+
+int
+main(int argc, char *argv[])
+{
+    setlocale(LC_ALL, "");
+
+    g_test_init(&argc, &argv, NULL);
+
+    g_test_set_nonfatal_assertions();
+
+    g_setenv("XDG_SESSION_DESKTOP", "", TRUE);
+    g_setenv("XDG_CURRENT_DESKTOP", "", TRUE);
+    g_setenv("GNOME_DESKTOP_SESSION_ID", "", TRUE);
+    g_setenv("KDE_FULL_SESSION", "", TRUE);
+    g_setenv("DESKTOP_SESSION", "", TRUE);
+
+    gsize i;
+    for ( i = 0 ; i < G_N_ELEMENTS(_nk_xdg_de_tests_list) ; ++i )
+        g_test_add_data_func(_nk_xdg_de_tests_list[i].testpath, &_nk_xdg_de_tests_list[i].data, _nk_xdg_de_tests_func);
+
+    int ret = g_test_run();
+    return ret;
+}
--- /dev/null
+++ rofi-1.6.0/subprojects/libnkutils/tests/xdg-theme.c
@@ -0,0 +1,533 @@
+/*
+ * libnkutils/token - Miscellaneous utilities, token module
+ *
+ * Copyright © 2011-2017 Quentin "Sardem FF7" Glidic
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <string.h>
+#include <locale.h>
+
+#include <glib.h>
+
+#include <nkutils-xdg-theme.h>
+
+#define MAX_THEMES 5
+
+static NkXdgThemeContext *context;
+typedef enum {
+    TYPE_ICON,
+    TYPE_SOUND,
+} NkXdgThemeTestType;
+
+typedef struct {
+    NkXdgThemeTestType type;
+    const gchar *themes[MAX_THEMES];
+    const gchar *name;
+    const gchar *context;
+    gint size;
+    gint scale;
+    gboolean svg;
+    const gchar *profile;
+    const gchar *result;
+    const gchar *theme_test;
+} NkXdgThemeTestData;
+
+static const struct {
+    const gchar *testpath;
+    NkXdgThemeTestData data;
+} _nk_uuid_tests_list[] = {
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/symbolic/found",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "zoom-in-symbolic",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/Adwaita/scalable/actions/zoom-in-symbolic.svg",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/symbolic/not-scalable",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "zoom-in-symbolic",
+            .size = 48,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/icons/Adwaita/48x48/actions/zoom-in-symbolic.symbolic.png",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/symbolic/found-no-symbolic",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "trophy-gold-symbolic",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/Adwaita/48x48/status/trophy-gold.png",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/theme-found/second-choice",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "hopefully-no-theme-has-this-name", [1] = "Adwaita" },
+            .name = "trophy-gold",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/Adwaita/48x48/status/trophy-gold.png",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/theme-found/fallback",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "geany",
+            .size = 16,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/icons/hicolor/16x16/apps/geany.png",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/context/exist-match/1",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "network-wireless-signal-ok-symbolic",
+            .context = "Status",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/Adwaita/scalable/status/network-wireless-signal-ok-symbolic.svg",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/context/exist-no-match",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "network-wireless-signal-ok-symbolic",
+            .context = "Applications",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = NULL,
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/context/exist-match/2",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "emblem-favorite-symbolic",
+            .context = "Emblems",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/Adwaita/scalable/emblems/emblem-favorite-symbolic.svg",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/gnome/symbolic/found",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "gnome" },
+            .name = "zoom-in-symbolic",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/gnome/scalable/actions/zoom-in-symbolic.svg",
+            .theme_test = "/usr/share/icons/gnome/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/gnome/symbolic/not-scalable",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "gnome" },
+            .name = "zoom-in-symbolic",
+            .size = 48,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/icons/gnome/48x48/actions/zoom-in.png",
+            .theme_test = "/usr/share/icons/gnome/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/gnome/symbolic/found-no-symbolic",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "gnome" },
+            .name = "trophy-gold-symbolic",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/gnome/48x48/status/trophy-gold.png",
+            .theme_test = "/usr/share/icons/gnome/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/gnome/theme-found/fallback",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "gnome" },
+            .name = "geany",
+            .size = 16,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/icons/hicolor/16x16/apps/geany.png",
+            .theme_test = "/usr/share/icons/gnome/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/gnome/context/exist-match/1",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "gnome" },
+            .name = "network-wireless-signal-ok-symbolic",
+            .context = "Status",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/gnome/scalable/status/network-wireless-signal-ok-symbolic.svg",
+            .theme_test = "/usr/share/icons/gnome/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/gnome/context/exist-no-match",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "gnome" },
+            .name = "network-wireless-signal-ok-symbolic",
+            .context = "Applications",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = NULL,
+            .theme_test = "/usr/share/icons/gnome/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/gnome/context/exist-match/2",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "gnome" },
+            .name = "emblem-favorite-symbolic",
+            .context = "Emblems",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/gnome/scalable/emblems/emblem-favorite-symbolic.svg",
+            .theme_test = "/usr/share/icons/gnome/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/no-theme/threshold/found/1",
+        .data = {
+            .type = TYPE_ICON,
+            .name = "geany",
+            .size = 18,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/icons/hicolor/16x16/apps/geany.png",
+            .theme_test = "/usr/share/icons/hicolor/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/no-theme/threshold/found/2",
+        .data = {
+            .type = TYPE_ICON,
+            .name = "pidgin",
+            .size = 18,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/icons/hicolor/16x16/apps/pidgin.png",
+            .theme_test = "/usr/share/icons/hicolor/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/no-theme/size/fallback/hicolor/1",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "pidgin",
+            .size = 0,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/hicolor/scalable/apps/pidgin.svg",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/no-theme/size/fallback/pixmaps/1",
+        .data = {
+            .type = TYPE_ICON,
+            .name = "htop",
+            .size = 19,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/pixmaps/htop.png",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/no-theme/size/fallback/pixmaps/2",
+        .data = {
+            .type = TYPE_ICON,
+            .name = "debian-logo",
+            .size = 0,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/pixmaps/debian-logo.png",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/size/biggest/fixed",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "edit-find-symbolic",
+            .size = 0,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/icons/Adwaita/96x96/actions/edit-find-symbolic.symbolic.png",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/size/biggest/svg",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "edit-find-symbolic",
+            .size = 0,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/Adwaita/scalable/actions/edit-find-symbolic.svg",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/Adwaita/size/best-distance",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "Adwaita" },
+            .name = "edit-find",
+            .size = 19,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/icons/Adwaita/22x22/actions/edit-find.png",
+            .theme_test = "/usr/share/icons/Adwaita/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/gnome/size/biggest/fixed",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "gnome" },
+            .name = "edit-find-symbolic",
+            .size = 0,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/icons/gnome/96x96/actions/edit-find-symbolic.symbolic.png",
+            .theme_test = "/usr/share/icons/gnome/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/gnome/size/biggest/svg",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "gnome" },
+            .name = "edit-find-symbolic",
+            .size = 0,
+            .scale = 1,
+            .svg = TRUE,
+            .result = "/usr/share/icons/gnome/scalable/actions/edit-find-symbolic.svg",
+            .theme_test = "/usr/share/icons/gnome/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/gnome/size/best-distance",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "gnome" },
+            .name = "edit-find",
+            .size = 19,
+            .scale = 1,
+            .svg = FALSE,
+            .result = "/usr/share/icons/gnome/22x22/actions/edit-find.png",
+            .theme_test = "/usr/share/icons/gnome/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/wrong-theme/not-found",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "do-not-exists-hopefully" },
+            .name = "nothing-on-earth-will-have-that-name",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = NULL,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/no-theme/not-found",
+        .data = {
+            .type = TYPE_ICON,
+            .name = "nothing-on-earth-will-have-that-name",
+            .size = 48,
+            .scale = 1,
+            .svg = TRUE,
+            .result = NULL,
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/icon/theme/recursive",
+        .data = {
+            .type = TYPE_ICON,
+            .themes = { [0] = "recursive-theme-test" },
+            .name = "test-icon",
+            .size = 10,
+            .scale = 1,
+            .svg = TRUE,
+            .result = SRCDIR G_DIR_SEPARATOR_S "tests/icons/recursive-theme-test/test-dir/test-icon.svg",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/sound/found/variant",
+        .data = {
+            .type = TYPE_SOUND,
+            .themes = { [0] = "freedesktop" },
+            .name = "network-connectivity-established",
+            .profile = "stereo",
+            .result = "/usr/share/sounds/freedesktop/stereo/network-connectivity-established.oga",
+            .theme_test = "/usr/share/sounds/freedesktop/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/sound/fallback/variant",
+        .data = {
+            .type = TYPE_SOUND,
+            .themes = { [0] = "freedesktop" },
+            .name = "bell-too-specific",
+            .profile = "stereo",
+            .result = "/usr/share/sounds/freedesktop/stereo/bell.oga",
+            .theme_test = "/usr/share/sounds/freedesktop/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/sound/fallback/freedesktop",
+        .data = {
+            .type = TYPE_SOUND,
+            .themes = { [0] = "non-existing-i-hope" },
+            .name = "dialog-information",
+            .profile = "stereo",
+            .result = "/usr/share/sounds/freedesktop/stereo/dialog-information.oga",
+            .theme_test = "/usr/share/sounds/freedesktop/index.theme",
+        }
+    },
+    {
+        .testpath = "/nkutils/xdg-theme/sound/fallback/no-index",
+        .data = {
+            .type = TYPE_SOUND,
+            .themes = { [0] = "purple" },
+            .name = "logout",
+            .profile = "stereo",
+            .result = "/usr/share/sounds/purple/logout.wav",
+        }
+    },
+};
+
+static void
+_nk_uuid_tests_func(gconstpointer user_data)
+{
+    const NkXdgThemeTestData *data = user_data;
+
+    if ( ( ( data->result != NULL ) && ( ! g_file_test(data->result, G_FILE_TEST_IS_REGULAR) ) )
+         || ( ( data->theme_test != NULL ) && ( ! g_file_test(data->theme_test, G_FILE_TEST_IS_REGULAR) ) ) )
+    {
+        g_test_skip("Theme not installed");
+        return;
+    }
+
+    gchar *file = NULL;
+    switch ( data->type )
+    {
+    case TYPE_ICON:
+        file = nk_xdg_theme_get_icon(context, data->themes, data->context, data->name, data->size, data->scale, data->svg);
+    break;
+    case TYPE_SOUND:
+        file = nk_xdg_theme_get_sound(context, data->themes, data->name, data->profile, NULL);
+    break;
+    }
+    g_assert_cmpstr(file, ==, data->result);
+    g_free(file);
+}
+
+int
+main(int argc, char *argv[])
+{
+    setlocale(LC_ALL, "");
+
+    g_test_init(&argc, &argv, NULL);
+
+    g_test_set_nonfatal_assertions();
+
+    g_setenv("HOME", "/var/empty", TRUE);
+    g_setenv("XDG_CONFIG_HOME", SRCDIR G_DIR_SEPARATOR_S "tests", TRUE);
+    g_setenv("XDG_DATA_HOME", SRCDIR G_DIR_SEPARATOR_S "tests", TRUE);
+    g_setenv("XDG_DATA_DIRS", "/usr/share/", TRUE);
+    g_setenv("XDG_SESSION_DESKTOP", "", TRUE);
+    g_setenv("XDG_CURRENT_DESKTOP", "", TRUE);
+    g_setenv("GNOME_DESKTOP_SESSION_ID", "", TRUE);
+    g_setenv("KDE_FULL_SESSION", "", TRUE);
+    g_setenv("DESKTOP_SESSION", "", TRUE);
+
+    gsize i;
+    for ( i = 0 ; i < G_N_ELEMENTS(_nk_uuid_tests_list) ; ++i )
+        g_test_add_data_func(_nk_uuid_tests_list[i].testpath, &_nk_uuid_tests_list[i].data, _nk_uuid_tests_func);
+
+    context = nk_xdg_theme_context_new(NULL, NULL);
+    int ret = g_test_run();
+    nk_xdg_theme_context_free(context);
+    return ret;
+}
