diff --git a/configure.ac b/configure.ac
index 6082c5e..60dee98 100644
--- a/configure.ac
+++ b/configure.ac
@@ -63,7 +63,7 @@ else
 fi
 
 dnl Test for required libraries
-pkg_modules="libfm >= 1.2.0 x11"
+pkg_modules="glib-2.0 libxml-2.0 x11"
 PKG_CHECK_MODULES(PACKAGE, [$pkg_modules])
 AC_SUBST(PACKAGE_CFLAGS)
 AC_SUBST(PACKAGE_LIBS)
@@ -96,9 +96,6 @@ AM_CONDITIONAL(WITH_GTK, [test x$with_gtk = xyes])
 AC_SUBST(GTK_CFLAGS)
 AC_SUBST(GTK_LIBS)
 
-dnl Test for libunistring for correct UTF-8 printf
-AC_CHECK_LIB(unistring, ulc_fprintf)
-
 dnl Supress extra linking
 AC_MSG_CHECKING([whether $LD accepts --as-needed])
 case `$LD --as-needed -v 2>&1 </dev/null` in
diff --git a/lxhotkey.pc.in b/lxhotkey.pc.in
index ba2933d..5c4e9f0 100644
--- a/lxhotkey.pc.in
+++ b/lxhotkey.pc.in
@@ -6,7 +6,6 @@ pluginsdir=${pkglibdir}
 
 Name: lxhotkey
 Description: A lightweight global keyboard shortcuts configurator.
-Requires: libfm >= 1.2.0
 Version: @VERSION@
 Libs: 
 Cflags: -I${includedir}
diff --git a/man/lxhotkey.1.in b/man/lxhotkey.1.in
index 7a2fdba..63c0f7c 100644
--- a/man/lxhotkey.1.in
+++ b/man/lxhotkey.1.in
@@ -8,9 +8,9 @@ LXHotkey \- A lightweight global keyboard shortcuts configurator
 .B lxhotkey
 [ command ]
 .SH DESCRIPTION
-\fBLXHotkey\fP is an universal application for the Lightweight X11 Desktop
-Environment (\fBLXDE\fP) which allows view and change global keyboard shortcuts
-for Window Manager actions, including ones to start applications.
+\fBLXHotkey\fP allows viewing and changing global keyboard shortcuts
+for supported window managers and compositors, including shortcuts
+used to start applications.
 .SH OPTIONS
 .SS Command line interface:
 .TP 20
@@ -38,6 +38,8 @@ start with GUI of \fItype\fR
 .SH SUPPORTED WINDOW MANAGERS
 .PD 0
 .TP
+.I labwc
+.TP
 .I Openbox
 
 .SH AUTHOR
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 315a631..520c16f 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -1,32 +1,5 @@
 ## Process this file with automake to produce Makefile.in
 
-## common flags for all plugins
-AM_CPPFLAGS = \
-	-I$(top_srcdir) \
-	-I$(top_srcdir)/src \
-	$(PACKAGE_CFLAGS)
-
-AM_LDFLAGS = \
-	-module -avoid-version -shared -export-dynamic -no-undefined \
-	-rpath $(libdir)/lxhotkey $(PACKAGE_LIBS)
-
-## modules list
-pkglibdir = $(libdir)/lxhotkey
-pkglib_LTLIBRARIES = ob.la
-if WITH_GTK
-pkglib_LTLIBRARIES += gtk.la
-endif
-
-## Openbox module
-ob_la_SOURCES = openbox/openbox.c
-ob_la_LIBADD = -lfm-extra
-
-# GTK module
-gtk_la_SOURCES = gtk/gtk.c gtk/edit.c
-gtk_la_CFLAGS = $(GTK_CFLAGS)
-gtk_la_LIBADD = $(GTK_LIBS)
-
-
 # .desktop files
 desktopdir=$(datadir)/applications
 desktop_in_files =
@@ -38,13 +11,8 @@ desktop_DATA = $(desktop_in_files:.desktop.in=.desktop)
 
 EXTRA_DIST = \
 	gtk/lxhotkey-gtk.desktop.in gtk/lxhotkey-gtk.desktop \
-	gtk/edit.h
+	gtk/edit.h \
+	gtk/gtk.c gtk/edit.c \
+	openbox/openbox.c openbox/xml_compat.h openbox/xml_compat.c
 
 DISTCLEANFILES = gtk/lxhotkey-gtk.desktop
-
-install-exec-hook:
-	rm -f $(DESTDIR)$(pkglibdir)/*.la
-
-PLUGINS_INSTALLED = $(pkglib_LTLIBRARIES:.la=.so)
-uninstall-hook:
-	cd $(DESTDIR)$(pkglibdir) && rm -f $(PLUGINS_INSTALLED) || true
diff --git a/plugins/gtk/gtk.c b/plugins/gtk/gtk.c
index c7c7ed3..5daa9a0 100644
--- a/plugins/gtk/gtk.c
+++ b/plugins/gtk/gtk.c
@@ -172,13 +172,17 @@ static void on_about(GtkAction *act, PluginData *data)
         "Andriy Grytsenko <andrej@rep.kiev.ua>",
         NULL
     };
+    const gchar *labwc[] = {
+        "sfs <sfslinux@gmail.com> + GPT-5.4",
+        NULL
+    };
     /* TRANSLATORS: Replace this string with your names, one name per line. */
     gchar *translators = _("translator-credits");
 
     about = GTK_ABOUT_DIALOG(gtk_about_dialog_new());
     gtk_window_set_icon_name(GTK_WINDOW(about), LXHOTKEY_ICON);
     gtk_about_dialog_set_version(about, VERSION);
-    gtk_about_dialog_set_program_name(about, "LXHotkey"); //FIXME: translated?
+    gtk_about_dialog_set_program_name(about, "labhotkey");
     gtk_about_dialog_set_logo_icon_name(about, LXHOTKEY_ICON);
 
     gtk_about_dialog_set_copyright(about, _("Copyright (C) 2016-2025"));
@@ -198,6 +202,7 @@ static void on_about(GtkAction *act, PluginData *data)
                                         "Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.");
     gtk_about_dialog_set_website(about, "http://lxde.org/");
     gtk_about_dialog_set_authors(about, authors);
+    gtk_about_dialog_add_credit_section(about, "labwc", labwc);
     gtk_about_dialog_set_translator_credits(about, translators);
     gtk_dialog_run(GTK_DIALOG(about));
     gtk_widget_destroy(GTK_WIDGET(about));
@@ -383,6 +387,7 @@ static void module_gtk_run(const gchar *wm, const LXHotkeyPluginInit *cb,
     win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
     gtk_window_set_default_size(GTK_WINDOW(win), 800, 480);
     gtk_window_set_icon_name(GTK_WINDOW(win), LXHOTKEY_ICON);
+    gtk_window_set_title(GTK_WINDOW(win), "labhotkey");
     g_signal_connect(win, "unmap", G_CALLBACK(gtk_main_quit), NULL);
 
     vbox = (GtkBox *)gtk_vbox_new(FALSE, 0);
@@ -513,8 +517,6 @@ static void module_gtk_init(int argc, char **argv)
     inited = 1;
 }
 
-FM_DEFINE_MODULE(lxhotkey_gui, gtk)
-
 LXHotkeyGUIPluginInit fm_module_init_lxhotkey_gui = {
     .run = module_gtk_run,
     .alert = module_gtk_alert,
diff --git a/plugins/gtk/edit.c b/plugins/gtk/edit.c
index 1bce084..e8e91ed 100644
--- a/plugins/gtk/edit.c
+++ b/plugins/gtk/edit.c
@@ -1143,12 +1143,10 @@ void _edit_action(PluginData *data, GError **error)
         gtk_alignment_set_padding(GTK_ALIGNMENT(align), 0, 0, 4, 0);
         gtk_container_add(GTK_CONTAINER(align), GTK_WIDGET(data->edit_exec));
         gtk_box_pack_start(xbox, align, FALSE, TRUE, 0);
-        align = gtk_alignment_new(0.0, 0.0, 0.0, 0.0);
-        gtk_container_add(GTK_CONTAINER(align), gtk_label_new(_("Options:")));
-        gtk_box_pack_start(xbox, align, FALSE, TRUE, 0);
     }
     data->edit_tree = GTK_TREE_VIEW(gtk_tree_view_new());
-    gtk_box_pack_start(xbox, GTK_WIDGET(data->edit_tree), TRUE, TRUE, 0);
+    if (is_action)
+        gtk_box_pack_start(xbox, GTK_WIDGET(data->edit_tree), TRUE, TRUE, 0);
     gtk_container_add(GTK_CONTAINER(widget), GTK_WIDGET(xbox));
     gtk_box_pack_start(vbox, widget, FALSE, TRUE, 0);
     gtk_tree_view_insert_column_with_attributes(data->edit_tree, 0, NULL,
@@ -1235,7 +1233,12 @@ void _edit_action(PluginData *data, GError **error)
     {
         /* AddAction is visible for action only */
         GtkAction *act = gtk_ui_manager_get_action(ui, "/toolbar/AddAction");
+
         gtk_action_set_visible(act, FALSE);
+        gtk_action_set_visible(data->add_option_button, FALSE);
+        gtk_action_set_visible(data->rm_option_button, FALSE);
+        gtk_action_set_visible(data->edit_option_button, FALSE);
+        gtk_action_set_visible(data->add_suboption_button, FALSE);
     }
     gtk_container_add(GTK_CONTAINER(data->edit_window), GTK_WIDGET(vbox));
     g_signal_connect(gtk_tree_view_get_selection(data->edit_tree), "changed",
@@ -1243,5 +1246,8 @@ void _edit_action(PluginData *data, GError **error)
     update_options_tree(data);
     update_edit_toolbar(data);
     gtk_window_present(data->edit_window);
-    gtk_widget_grab_focus(GTK_WIDGET(data->edit_tree));
+    if (data->edit_exec)
+        gtk_widget_grab_focus(GTK_WIDGET(data->edit_exec));
+    else
+        gtk_widget_grab_focus(GTK_WIDGET(data->edit_tree));
 }
diff --git a/plugins/openbox/openbox.c b/plugins/openbox/openbox.c
index 74ee21a..aff16cc 100644
--- a/plugins/openbox/openbox.c
+++ b/plugins/openbox/openbox.c
@@ -30,7 +30,8 @@
 #include <X11/Xlib.h>
 #include <X11/Xatom.h>
 
-#include <libfm/fm-extra.h>
+#include <signal.h>
+#include "xml_compat.h"
 #include <glib/gi18n.h>
 
 #include <fnmatch.h>
@@ -50,6 +51,47 @@ enum LXHotkeyObError {
     LXKEYS_PARSE_ERROR
 };
 
+static gboolean string_mentions_labwc(const char *value)
+{
+    char *lower;
+    gboolean ret;
+
+    if (value == NULL || value[0] == '\0')
+        return FALSE;
+    lower = g_ascii_strdown(value, -1);
+    ret = g_strrstr(lower, "labwc") != NULL;
+    g_free(lower);
+    return ret;
+}
+
+static gboolean session_is_labwc(void)
+{
+    return (g_getenv("LABWC_PID") != NULL && g_getenv("LABWC_PID")[0] != '\0')
+        || string_mentions_labwc(g_getenv("DESKTOP_SESSION"))
+        || string_mentions_labwc(g_getenv("GDMSESSION"))
+        || string_mentions_labwc(g_getenv("XDG_CURRENT_DESKTOP"));
+}
+
+static gboolean reconfigure_labwc(GError **error)
+{
+    const char *pid = g_getenv("LABWC_PID");
+    gint status = 0;
+
+    if (pid != NULL && pid[0] != '\0') {
+        gint64 labwc_pid = g_ascii_strtoll(pid, NULL, 10);
+
+        if (labwc_pid > 0 && kill((int)labwc_pid, SIGHUP) == 0)
+            return TRUE;
+    }
+    if (g_spawn_command_line_sync("labwc --reconfigure", NULL, NULL, &status, NULL)
+        && g_spawn_check_exit_status(status, NULL))
+        return TRUE;
+
+    g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                        _("Failed to reconfigure labwc."));
+    return FALSE;
+}
+
 
 /* simple functions to manage LXHotkeyAttr data type */
 static inline LXHotkeyAttr *lxhotkey_attr_new(void)
@@ -168,6 +210,12 @@ static gboolean restart_openbox(GError **error)
     XEvent ce;
     gboolean ret = TRUE;
 
+    if (dpy == NULL) {
+        g_set_error_literal(error, LXKEYS_OB_ERROR, LXKEYS_FILE_ERROR,
+                            _("Failed to reconfigure Openbox."));
+        return FALSE;
+    }
+
     ce.xclient.type = ClientMessage;
     ce.xclient.message_type = XInternAtom(dpy, "_OB_CONTROL", True);
     ce.xclient.display = dpy;
@@ -251,17 +303,7 @@ static gboolean restart_openbox(GError **error)
 
 static char * values_enabled[] = { N_("yes"), N_("no"), NULL };
 
-static LXHotkeyAttr options_startupnotify[] = {
-    { N_("enabled"), BOOLEAN_VALUES, NULL, NULL, FALSE },
-    { N_("wmclass"), NULL, NULL, NULL, FALSE },
-    { N_("name"), NULL, NULL, NULL, FALSE },
-    { N_("icon"), NULL, NULL, NULL, FALSE },
-    { NULL }
-};
-
 static LXHotkeyAttr options_Execute[] = {
     { N_("command"), NULL, NULL, NULL, FALSE },
-    { N_("prompt"), NULL, NULL, NULL, FALSE },
-    { N_("startupnotify"), NULL, TO_BE_CONVERTED(options_startupnotify), NULL, FALSE },
     { NULL }
 };
@@ -334,6 +376,5 @@ static LXHotkeyAttr options_Restart[] = {
 };
 
 static LXHotkeyAttr options_Exit[] = {
-    { N_("prompt"), BOOLEAN_VALUES, NULL, NULL, FALSE },
     { NULL }
 };
@@ -480,6 +528,7 @@ typedef struct {
 
 typedef struct {
     char *path;
+    gboolean is_labwc;
     FmXmlFile *xml;
     FmXmlFileItem *keyboard; /* the <keyboard> section */
     GList *actions; /* no-exec actions, in reverse order */
@@ -819,7 +868,7 @@ static void obcfg_free(gpointer config)
     ObXmlFile *cfg = (ObXmlFile *)config;
 
     g_free(cfg->path);
-    g_object_unref(cfg->xml);
+    fm_xml_file_free(cfg->xml);
     g_list_free_full(cfg->actions, (GDestroyNotify)lkxeys_action_free);
     g_list_free_full(cfg->execs, (GDestroyNotify)lkxeys_app_free);
     clear_stack(cfg);
@@ -839,7 +888,7 @@ static gpointer obcfg_load(gpointer config, GError **error)
         FmXmlFile *old_xml = cfg->xml;
 
         cfg->xml = fm_xml_file_new(old_xml);
-        g_object_unref(old_xml);
+        fm_xml_file_free(old_xml);
         g_list_free_full(cfg->actions, (GDestroyNotify)lkxeys_action_free);
         g_list_free_full(cfg->execs, (GDestroyNotify)lkxeys_app_free);
         cfg->actions = NULL;
@@ -847,6 +896,7 @@ static gpointer obcfg_load(gpointer config, GError **error)
         cfg->keyboard = NULL;
     } else {
         const char *session;
+        char *labwc_path;
 
         /* prepare data */
         cfg = g_new0(ObXmlFile, 1);
@@ -869,6 +919,7 @@ static gpointer obcfg_load(gpointer config, GError **error)
             session = g_getenv("GDMSESSION");
         if (session == NULL)
             session = g_getenv("XDG_CURRENT_DESKTOP");
+        labwc_path = g_build_filename(g_get_user_config_dir(), "labwc", "rc.xml", NULL);
         if (g_strcmp0(session, "Lubuntu") == 0)
             cfg->path = g_build_filename(g_get_user_config_dir(), "openbox",
                                          "lubuntu-rc.xml", NULL);
@@ -878,19 +929,26 @@ static gpointer obcfg_load(gpointer config, GError **error)
         else if (g_strcmp0(session, "LXDE-pi") == 0)
             cfg->path = g_build_filename(g_get_user_config_dir(), "openbox",
                                          "lxde-pi-rc.xml", NULL);
+        else if (session_is_labwc() || g_file_test(labwc_path, G_FILE_TEST_EXISTS)) {
+            cfg->path = labwc_path;
+            cfg->is_labwc = TRUE;
+            labwc_path = NULL;
+        }
         else
             cfg->path = g_build_filename(g_get_user_config_dir(), "openbox",
                                          "rc.xml", NULL);
+        g_free(labwc_path);
     }
 
-    /* try to load ~/.config/openbox/$xml */
+    /* try to load the user rc.xml first */
     if (!g_file_get_contents(cfg->path, &contents, &len, NULL)) {
-        /* if it does not exist then try to load $XDG_SYSTEM_CONFDIR/openbox/rc.xml */
+        /* if it does not exist then try system rc.xml in XDG config dirs */
         const gchar * const *dirs;
         char *path = NULL;
 
         for (dirs = g_get_system_config_dirs(); dirs[0]; dirs++) {
-            path = g_build_filename(dirs[0], "openbox", "rc.xml", NULL);
+            path = g_build_filename(dirs[0], cfg->is_labwc ? "labwc" : "openbox",
+                                    "rc.xml", NULL);
             if (g_file_get_contents(path, &contents, &len, NULL))
                 break;
             g_free(path);
@@ -923,7 +981,7 @@ static gboolean obcfg_save(gpointer config, GError **error)
     gsize len;
     gboolean ret = FALSE;
 
-    /* save as ~/.config/openbox/$xml */
+    /* save as ~/.config/{openbox,labwc}/$xml */
     contents = fm_xml_file_to_data(cfg->xml, &len, error);
     if (contents) {
         /* workaround on libfm-extra bug on save data without DTD */
@@ -934,7 +992,7 @@ static gboolean obcfg_save(gpointer config, GError **error)
         g_free(contents);
     }
     if (ret)
-        ret = restart_openbox(error);
+        ret = cfg->is_labwc ? reconfigure_labwc(error) : restart_openbox(error);
     return ret;
 }
 
@@ -974,7 +1032,7 @@ static FmXmlFileItem *make_new_xml_item(ObXmlFile *cfg, LXHotkeyAttr *opt,
     LXHotkeyAttr *act = NULL;
 
     if (is_action) {
-        item = fm_xml_file_item_new(ObXmlFile_action);
+        item = fm_xml_file_item_new(cfg->xml, ObXmlFile_action);
         fm_xml_file_item_set_attribute(item, "name", opt->name);
     } else {
         FmXmlFileTag tag = FM_XML_FILE_TAG_NOT_HANDLED;
@@ -990,7 +1048,7 @@ static FmXmlFileItem *make_new_xml_item(ObXmlFile *cfg, LXHotkeyAttr *opt,
             cfg->added_tags = g_list_prepend(cfg->added_tags, GUINT_TO_POINTER(tag));
         } else
             tag = GPOINTER_TO_UINT(l->data);
-        item = fm_xml_file_item_new(tag);
+        item = fm_xml_file_item_new(cfg->xml, tag);
         if (opt->values)
             fm_xml_file_item_append_text(item, opt->values->data, -1, FALSE);
     }
@@ -1016,7 +1074,7 @@ static FmXmlFileItem *make_new_xml_binding(ObXmlFile *cfg, GList *actions,
                                            const gchar *accel, GList **opts,
                                            const gchar *exec)
 {
-    FmXmlFileItem *binding = fm_xml_file_item_new(ObXmlFile_keybind);
+    FmXmlFileItem *binding = fm_xml_file_item_new(cfg->xml, ObXmlFile_keybind);
     FmXmlFileItem *item, *opt;
     char *obkey = key_to_obkey(accel);
 
@@ -1026,10 +1084,10 @@ static FmXmlFileItem *make_new_xml_binding(ObXmlFile *cfg, GList *actions,
     if (exec) {
         /* make <action name='Execute'><command>exec</command>..opts..</action>
            instead of ..<acts>.. */
-        item = fm_xml_file_item_new(ObXmlFile_action);
+        item = fm_xml_file_item_new(cfg->xml, ObXmlFile_action);
         fm_xml_file_item_set_attribute(item, "name", "Execute");
         fm_xml_file_item_append_child(binding, item);
-        opt = fm_xml_file_item_new(ObXmlFile_command);
+        opt = fm_xml_file_item_new(cfg->xml, ObXmlFile_command);
         fm_xml_file_item_append_text(opt, exec, -1, FALSE);
         fm_xml_file_item_append_child(item, opt);
     }
@@ -1395,8 +1453,6 @@ static GList *obcfg_get_app_options(gpointer config, GError **error)
 }
 
 
-FM_DEFINE_MODULE(lxhotkey, Openbox)
-
 LXHotkeyPluginInit fm_module_init_lxhotkey = {
     .load = obcfg_load,
     .save = obcfg_save,
diff --git a/plugins/openbox/xml_compat.c b/plugins/openbox/xml_compat.c
new file mode 100644
index 0000000..64599c8
--- /dev/null
+++ b/plugins/openbox/xml_compat.c
@@ -0,0 +1,425 @@
+#include "xml_compat.h"
+
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+
+struct _FmXmlFileItem {
+    FmXmlFile *file;
+    xmlNodePtr node;
+};
+
+typedef struct {
+    char *name;
+    FmXmlFileHandler handler;
+} FmXmlTagDef;
+
+struct _FmXmlFile {
+    GPtrArray *tags;
+    GHashTable *tag_by_name;
+    xmlDocPtr doc;
+};
+
+static void free_tag_def(gpointer data)
+{
+    FmXmlTagDef *tag = data;
+
+    if (tag == NULL)
+        return;
+    g_free(tag->name);
+    g_free(tag);
+}
+
+static GQuark lxhotkey_xml_error_quark(void)
+{
+    return g_quark_from_static_string("lxhotkey-xml-error");
+}
+
+static void init_registry(FmXmlFile *file)
+{
+    FmXmlTagDef *tag;
+
+    file->tags = g_ptr_array_new_with_free_func(free_tag_def);
+    file->tag_by_name = g_hash_table_new(g_str_hash, g_str_equal);
+
+    g_ptr_array_add(file->tags, NULL);
+
+    tag = g_new0(FmXmlTagDef, 1);
+    tag->name = g_strdup("#text");
+    g_ptr_array_add(file->tags, tag);
+}
+
+static FmXmlFileItem *wrap_node(FmXmlFile *file, xmlNodePtr node)
+{
+    FmXmlFileItem *item;
+
+    if (node == NULL)
+        return NULL;
+    item = node->_private;
+    if (item != NULL)
+        return item;
+    item = g_new0(FmXmlFileItem, 1);
+    item->file = file;
+    item->node = node;
+    node->_private = item;
+    return item;
+}
+
+static void free_wrappers(xmlNodePtr node)
+{
+    xmlNodePtr child;
+    xmlNodePtr next;
+
+    for (child = node ? node->children : NULL; child; child = next) {
+        next = child->next;
+        free_wrappers(child);
+    }
+    if (node && node->_private) {
+        g_free(node->_private);
+        node->_private = NULL;
+    }
+}
+
+static void free_doc(FmXmlFile *file)
+{
+    if (file->doc == NULL)
+        return;
+    free_wrappers(xmlDocGetRootElement(file->doc));
+    xmlFreeDoc(file->doc);
+    file->doc = NULL;
+}
+
+static FmXmlTagDef *get_tag_def(FmXmlFile *file, FmXmlFileTag tag)
+{
+    if (file == NULL || tag >= file->tags->len)
+        return NULL;
+    return g_ptr_array_index(file->tags, tag);
+}
+
+static FmXmlFileTag get_tag_by_name(FmXmlFile *file, const char *name)
+{
+    gpointer value;
+
+    if (name == NULL)
+        return FM_XML_FILE_TAG_NOT_HANDLED;
+    value = g_hash_table_lookup(file->tag_by_name, name);
+    if (value == NULL)
+        return FM_XML_FILE_TAG_NOT_HANDLED;
+    return GPOINTER_TO_UINT(value);
+}
+
+static gboolean text_is_blank(xmlNodePtr node)
+{
+    const xmlChar *content;
+
+    if (node == NULL || node->type != XML_TEXT_NODE)
+        return FALSE;
+    content = node->content;
+    if (content == NULL)
+        return TRUE;
+    for (; *content; content++)
+        if (!g_ascii_isspace(*content))
+            return FALSE;
+    return TRUE;
+}
+
+static GList *node_children(FmXmlFile *file, xmlNodePtr node)
+{
+    GList *children = NULL;
+    xmlNodePtr child;
+
+    for (child = node ? node->children : NULL; child; child = child->next) {
+        if (child->type != XML_ELEMENT_NODE && child->type != XML_TEXT_NODE)
+            continue;
+        if (text_is_blank(child))
+            continue;
+        children = g_list_append(children, wrap_node(file, child));
+    }
+    return children;
+}
+
+static gboolean run_handlers(FmXmlFile *file, xmlNodePtr node, GError **error,
+                             gpointer user_data)
+{
+    xmlNodePtr child;
+    xmlNodePtr next;
+    FmXmlFileItem *item;
+    FmXmlTagDef *tag;
+    FmXmlFileTag tag_id;
+    GList *children;
+    char **attr_names = NULL;
+    char **attr_values = NULL;
+    guint n_attrs = 0;
+    guint i = 0;
+    xmlAttrPtr attr;
+
+    if (node == NULL)
+        return TRUE;
+    for (child = node->children; child; child = next) {
+        next = child->next;
+        if (child->type == XML_ELEMENT_NODE) {
+            if (!run_handlers(file, child, error, user_data))
+                return FALSE;
+        }
+    }
+    if (node->type != XML_ELEMENT_NODE)
+        return TRUE;
+
+    tag_id = get_tag_by_name(file, (const char *)node->name);
+    tag = get_tag_def(file, tag_id);
+    if (tag == NULL || tag->handler == NULL)
+        return TRUE;
+
+    for (attr = node->properties; attr; attr = attr->next)
+        n_attrs++;
+    if (n_attrs > 0) {
+        attr_names = g_new0(char *, n_attrs + 1);
+        attr_values = g_new0(char *, n_attrs + 1);
+        for (attr = node->properties; attr; attr = attr->next, i++) {
+            xmlChar *value = xmlNodeListGetString(node->doc, attr->children, 1);
+
+            attr_names[i] = g_strdup((const char *)attr->name);
+            attr_values[i] = g_strdup((const char *)(value ? value : BAD_CAST ""));
+            if (value)
+                xmlFree(value);
+        }
+    }
+
+    item = wrap_node(file, node);
+    children = node_children(file, node);
+    if (!tag->handler(item, children, attr_names, attr_values, n_attrs,
+                      (gint)xmlGetLineNo(node), 0, error, user_data)) {
+        g_list_free(children);
+        g_strfreev(attr_names);
+        g_strfreev(attr_values);
+        return FALSE;
+    }
+    g_list_free(children);
+    g_strfreev(attr_names);
+    g_strfreev(attr_values);
+    return TRUE;
+}
+
+FmXmlFile *fm_xml_file_new(FmXmlFile *other)
+{
+    FmXmlFile *file = g_new0(FmXmlFile, 1);
+    guint i;
+
+    init_registry(file);
+    if (other == NULL)
+        return file;
+    for (i = 2; i < other->tags->len; i++) {
+        FmXmlTagDef *old = g_ptr_array_index(other->tags, i);
+        FmXmlTagDef *tag = g_new0(FmXmlTagDef, 1);
+
+        tag->name = g_strdup(old->name);
+        tag->handler = old->handler;
+        g_ptr_array_add(file->tags, tag);
+        g_hash_table_insert(file->tag_by_name, tag->name, GUINT_TO_POINTER(i));
+    }
+    return file;
+}
+
+void fm_xml_file_free(FmXmlFile *file)
+{
+    if (file == NULL)
+        return;
+    free_doc(file);
+    g_ptr_array_free(file->tags, TRUE);
+    g_hash_table_destroy(file->tag_by_name);
+    g_free(file);
+}
+
+FmXmlFileTag fm_xml_file_set_handler(FmXmlFile *file, const char *name,
+                                     FmXmlFileHandler handler, gboolean recursive,
+                                     gpointer unused)
+{
+    FmXmlFileTag tag_id;
+    FmXmlTagDef *tag;
+
+    (void)recursive;
+    (void)unused;
+
+    tag_id = get_tag_by_name(file, name);
+    if (tag_id != FM_XML_FILE_TAG_NOT_HANDLED) {
+        tag = get_tag_def(file, tag_id);
+        tag->handler = handler;
+        return tag_id;
+    }
+
+    tag = g_new0(FmXmlTagDef, 1);
+    tag->name = g_strdup(name);
+    tag->handler = handler;
+    tag_id = file->tags->len;
+    g_ptr_array_add(file->tags, tag);
+    g_hash_table_insert(file->tag_by_name, tag->name, GUINT_TO_POINTER(tag_id));
+    return tag_id;
+}
+
+const char *fm_xml_file_get_tag_name(FmXmlFile *file, FmXmlFileTag tag)
+{
+    FmXmlTagDef *tag_def = get_tag_def(file, tag);
+
+    return tag_def ? tag_def->name : NULL;
+}
+
+gboolean fm_xml_file_parse_data(FmXmlFile *file, const char *contents, gsize len,
+                                GError **error, gpointer user_data)
+{
+    xmlNodePtr root;
+
+    free_doc(file);
+    xmlResetLastError();
+    file->doc = xmlReadMemory(contents, (int)len, NULL, NULL,
+                              XML_PARSE_NOBLANKS | XML_PARSE_NONET);
+    if (file->doc == NULL) {
+        const xmlError *xml_error = xmlGetLastError();
+
+        g_set_error(error, lxhotkey_xml_error_quark(), 0,
+                    "%s", xml_error && xml_error->message ? xml_error->message
+                                                             : "Failed to parse XML.");
+        return FALSE;
+    }
+    root = xmlDocGetRootElement(file->doc);
+    if (root == NULL) {
+        g_set_error_literal(error, lxhotkey_xml_error_quark(), 0,
+                            "Failed to parse XML.");
+        return FALSE;
+    }
+    wrap_node(file, root);
+    return run_handlers(file, root, error, user_data);
+}
+
+FmXmlFileItem *fm_xml_file_finish_parse(FmXmlFile *file, GError **error)
+{
+    (void)error;
+    return wrap_node(file, xmlDocGetRootElement(file->doc));
+}
+
+char *fm_xml_file_to_data(FmXmlFile *file, gsize *len, GError **error)
+{
+    xmlChar *buffer = NULL;
+    int size = 0;
+    char *copy;
+
+    (void)error;
+    xmlDocDumpFormatMemoryEnc(file->doc, &buffer, &size, "UTF-8", 1);
+    if (buffer == NULL)
+        return NULL;
+    copy = g_strndup((const char *)buffer, size);
+    xmlFree(buffer);
+    if (len)
+        *len = (gsize)strlen(copy);
+    return copy;
+}
+
+FmXmlFileTag fm_xml_file_item_get_tag(FmXmlFileItem *item)
+{
+    if (item == NULL || item->node == NULL)
+        return FM_XML_FILE_TAG_NOT_HANDLED;
+    if (item->node->type == XML_TEXT_NODE)
+        return FM_XML_FILE_TEXT;
+    return get_tag_by_name(item->file, (const char *)item->node->name);
+}
+
+const char *fm_xml_file_item_get_tag_name(FmXmlFileItem *item)
+{
+    if (item == NULL || item->node == NULL)
+        return NULL;
+    if (item->node->type == XML_TEXT_NODE)
+        return "#text";
+    return (const char *)item->node->name;
+}
+
+GList *fm_xml_file_item_get_children(FmXmlFileItem *item)
+{
+    if (item == NULL)
+        return NULL;
+    return node_children(item->file, item->node);
+}
+
+FmXmlFileItem *fm_xml_file_item_get_parent(FmXmlFileItem *item)
+{
+    if (item == NULL || item->node == NULL || item->node->parent == NULL)
+        return NULL;
+    if (item->node->parent->type == XML_DOCUMENT_NODE)
+        return NULL;
+    return wrap_node(item->file, item->node->parent);
+}
+
+FmXmlFileItem *fm_xml_file_item_find_child(FmXmlFileItem *item, FmXmlFileTag tag)
+{
+    xmlNodePtr child;
+
+    if (item == NULL)
+        return NULL;
+    for (child = item->node->children; child; child = child->next) {
+        FmXmlFileItem *wrapped;
+
+        if (child->type != XML_ELEMENT_NODE && child->type != XML_TEXT_NODE)
+            continue;
+        if (text_is_blank(child))
+            continue;
+        wrapped = wrap_node(item->file, child);
+        if (fm_xml_file_item_get_tag(wrapped) == tag)
+            return wrapped;
+    }
+    return NULL;
+}
+
+const char *fm_xml_file_item_get_data(FmXmlFileItem *item, gpointer unused)
+{
+    (void)unused;
+    if (item == NULL || item->node == NULL)
+        return NULL;
+    return (const char *)item->node->content;
+}
+
+FmXmlFileItem *fm_xml_file_item_new(FmXmlFile *file, FmXmlFileTag tag)
+{
+    const char *name = fm_xml_file_get_tag_name(file, tag);
+    xmlNodePtr node;
+
+    if (name == NULL || tag == FM_XML_FILE_TEXT)
+        return NULL;
+    node = xmlNewNode(NULL, BAD_CAST name);
+    return wrap_node(file, node);
+}
+
+void fm_xml_file_item_set_attribute(FmXmlFileItem *item, const char *name,
+                                    const char *value)
+{
+    if (item == NULL || item->node == NULL)
+        return;
+    xmlSetProp(item->node, BAD_CAST name, BAD_CAST value);
+}
+
+void fm_xml_file_item_append_text(FmXmlFileItem *item, const char *text, gssize len,
+                                  gboolean escaped)
+{
+    xmlNodePtr node;
+
+    (void)escaped;
+    if (item == NULL || item->node == NULL)
+        return;
+    if (len < 0)
+        len = (gssize)strlen(text);
+    node = xmlNewTextLen(BAD_CAST text, (int)len);
+    xmlAddChild(item->node, node);
+    wrap_node(item->file, node);
+}
+
+void fm_xml_file_item_append_child(FmXmlFileItem *item, FmXmlFileItem *child)
+{
+    if (item == NULL || child == NULL)
+        return;
+    xmlAddChild(item->node, child->node);
+}
+
+void fm_xml_file_item_destroy(FmXmlFileItem *item)
+{
+    if (item == NULL || item->node == NULL)
+        return;
+    xmlUnlinkNode(item->node);
+    free_wrappers(item->node);
+    xmlFreeNode(item->node);
+}
diff --git a/plugins/openbox/xml_compat.h b/plugins/openbox/xml_compat.h
new file mode 100644
index 0000000..ead46c8
--- /dev/null
+++ b/plugins/openbox/xml_compat.h
@@ -0,0 +1,44 @@
+#ifndef LXHOTKEY_XML_COMPAT_H
+#define LXHOTKEY_XML_COMPAT_H
+
+#include <glib.h>
+
+typedef struct _FmXmlFile FmXmlFile;
+typedef struct _FmXmlFileItem FmXmlFileItem;
+typedef guint FmXmlFileTag;
+
+#define FM_XML_FILE_TAG_NOT_HANDLED 0
+#define FM_XML_FILE_TEXT 1
+
+typedef gboolean (*FmXmlFileHandler)(FmXmlFileItem *item, GList *children,
+                                     char * const *attribute_names,
+                                     char * const *attribute_values,
+                                     guint n_attributes, gint line, gint pos,
+                                     GError **error, gpointer user_data);
+
+FmXmlFile *fm_xml_file_new(FmXmlFile *other);
+void fm_xml_file_free(FmXmlFile *file);
+FmXmlFileTag fm_xml_file_set_handler(FmXmlFile *file, const char *name,
+                                     FmXmlFileHandler handler, gboolean recursive,
+                                     gpointer unused);
+const char *fm_xml_file_get_tag_name(FmXmlFile *file, FmXmlFileTag tag);
+gboolean fm_xml_file_parse_data(FmXmlFile *file, const char *contents, gsize len,
+                                GError **error, gpointer user_data);
+FmXmlFileItem *fm_xml_file_finish_parse(FmXmlFile *file, GError **error);
+char *fm_xml_file_to_data(FmXmlFile *file, gsize *len, GError **error);
+
+FmXmlFileTag fm_xml_file_item_get_tag(FmXmlFileItem *item);
+const char *fm_xml_file_item_get_tag_name(FmXmlFileItem *item);
+GList *fm_xml_file_item_get_children(FmXmlFileItem *item);
+FmXmlFileItem *fm_xml_file_item_get_parent(FmXmlFileItem *item);
+FmXmlFileItem *fm_xml_file_item_find_child(FmXmlFileItem *item, FmXmlFileTag tag);
+const char *fm_xml_file_item_get_data(FmXmlFileItem *item, gpointer unused);
+FmXmlFileItem *fm_xml_file_item_new(FmXmlFile *file, FmXmlFileTag tag);
+void fm_xml_file_item_set_attribute(FmXmlFileItem *item, const char *name,
+                                    const char *value);
+void fm_xml_file_item_append_text(FmXmlFileItem *item, const char *text, gssize len,
+                                  gboolean escaped);
+void fm_xml_file_item_append_child(FmXmlFileItem *item, FmXmlFileItem *child);
+void fm_xml_file_item_destroy(FmXmlFileItem *item);
+
+#endif
diff --git a/src/Makefile.am b/src/Makefile.am
index 6900326..9f8c4bd 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -4,13 +4,28 @@ bin_PROGRAMS = lxhotkey
 
 lxhotkey_CPPFLAGS = \
 	-I$(top_srcdir) \
-	-DPACKAGE_PLUGINS_DIR=\""$(libdir)/lxhotkey"\" \
-	-DPACKAGE_LOCALE_DIR=\""$(prefix)/$(DATADIRNAME)/locale"\" \
+	-I$(top_srcdir)/src \
+	-I$(top_srcdir)/plugins/openbox \
+	-DPACKAGE_LOCALE_DIR=\"$(prefix)/$(DATADIRNAME)/locale\" \
 	$(PACKAGE_CFLAGS)
 
-lxhotkey_SOURCES = lxhotkey.c
+lxhotkey_SOURCES = \
+	lxhotkey.c \
+	$(top_srcdir)/plugins/openbox/openbox.c \
+	$(top_srcdir)/plugins/openbox/xml_compat.c
 
+if WITH_GTK
+lxhotkey_CPPFLAGS += \
+	-I$(top_srcdir)/plugins/gtk \
+	-DWITH_GTK=1 \
+	$(GTK_CFLAGS)
+lxhotkey_SOURCES += \
+	$(top_srcdir)/plugins/gtk/gtk.c \
+	$(top_srcdir)/plugins/gtk/edit.c
+lxhotkey_LDADD = $(PACKAGE_LIBS) $(GTK_LIBS)
+else
 lxhotkey_LDADD = $(PACKAGE_LIBS)
+endif
 
 lxhotkey_includedir = $(includedir)/lxhotkey
 lxhotkey_include_HEADERS = lxhotkey.h
+
+install-exec-hook:
+	ln -sf lxhotkey "$(DESTDIR)$(bindir)/labhotkey"
+
+uninstall-hook:
+	rm -f "$(DESTDIR)$(bindir)/labhotkey"
diff --git a/src/lxhotkey.c b/src/lxhotkey.c
index a7512a7..e704d6a 100644
--- a/src/lxhotkey.c
+++ b/src/lxhotkey.c
@@ -30,6 +30,8 @@
 #include <X11/Xlib.h>
 #include <X11/Xatom.h>
 
+#include <locale.h>
+#include <stdio.h>
 #include <stdlib.h>
 
 #ifdef HAVE_LIBUNISTRING
@@ -124,6 +125,19 @@ static gchar *get_property (Display *disp, Window win, /*{{{*/
     return ret;
 } /*}}}*/
 
+static gboolean string_mentions_labwc(const char *value)
+{
+    char *lower;
+    gboolean ret;
+
+    if (value == NULL || value[0] == '\0')
+        return FALSE;
+    lower = g_ascii_strdown(value, -1);
+    ret = g_strrstr(lower, "labwc") != NULL;
+    g_free(lower);
+    return ret;
+}
+
 static gchar *get_wm_info(void)
 {
     /* this code is taken from wmctrl utility, adapted
@@ -132,6 +146,12 @@ static gchar *get_wm_info(void)
     Window *sup_window = NULL;
     gchar *wm_name = NULL;
 
+    if (g_getenv("LABWC_PID") != NULL
+        || string_mentions_labwc(g_getenv("DESKTOP_SESSION"))
+        || string_mentions_labwc(g_getenv("GDMSESSION"))
+        || string_mentions_labwc(g_getenv("XDG_CURRENT_DESKTOP")))
+        return g_strdup("labwc");
+
     if (!(disp = XOpenDisplay(NULL))) {
         fputs("Cannot open display.\n", stderr);
         return NULL;
@@ -143,6 +163,7 @@ static gchar *get_wm_info(void)
                         XA_CARDINAL, "_WIN_SUPPORTING_WM_CHECK", NULL))) {
             fputs("Cannot get window manager info properties.\n"
                   "(_NET_SUPPORTING_WM_CHECK or _WIN_SUPPORTING_WM_CHECK)\n", stderr);
+            XCloseDisplay(disp);
             return NULL;
         }
     }
@@ -165,6 +186,7 @@ static gchar *get_wm_info(void)
         }
     }
 
+    XCloseDisplay(disp);
     return wm_name;
 }
 
@@ -175,6 +197,9 @@ static gboolean test_X_is_local(void)
     int Xnum;
     char lockfile[32], socket[32];
 
+    if (g_getenv("WAYLAND_DISPLAY") != NULL)
+        return TRUE;
+
     if (display)
         display = strchr(display, ':');
     if (!display || display[1] < '0' || display[1] > '9')
@@ -198,19 +223,14 @@ typedef struct LXHotkeyPlugin {
 
 static LXHotkeyPlugin *plugins = NULL;
 
-FM_MODULE_DEFINE_TYPE(lxhotkey, LXHotkeyPluginInit, 1)
-static gboolean fm_module_callback_lxhotkey(const char *name, gpointer init, int ver)
+static void register_plugin(const char *name, LXHotkeyPluginInit *init)
 {
-    LXHotkeyPlugin *plugin;
+    LXHotkeyPlugin *plugin = g_new(LXHotkeyPlugin, 1);
 
-    /* ignore ver for now, only 1 exists */
-    /* FIXME: need to check for duplicates? */
-    plugin = g_new(LXHotkeyPlugin, 1);
     plugin->next = plugins;
     plugin->name = g_strdup(name);
     plugin->t = init;
     plugins = plugin;
-    return TRUE;
 }
 
 
@@ -223,19 +243,25 @@ typedef struct LXHotkeyGUIPlugin {
 
 static LXHotkeyGUIPlugin *gui_plugins = NULL;
 
-FM_MODULE_DEFINE_TYPE(lxhotkey_gui, LXHotkeyGUIPluginInit, 1)
-static gboolean fm_module_callback_lxhotkey_gui(const char *name, gpointer init, int ver)
+static void register_gui_plugin(const char *name, LXHotkeyGUIPluginInit *init)
 {
-    LXHotkeyGUIPlugin *plugin;
+    LXHotkeyGUIPlugin *plugin = g_new(LXHotkeyGUIPlugin, 1);
 
-    /* ignore ver for now, only 1 exists */
-    /* FIXME: need to check for duplicates? */
-    plugin = g_new(LXHotkeyGUIPlugin, 1);
     plugin->next = gui_plugins;
     plugin->name = g_strdup(name);
     plugin->t = init;
     gui_plugins = plugin;
-    return TRUE;
+}
+
+static void register_builtin_plugins(gboolean do_gui)
+{
+    register_plugin("Openbox", &fm_module_init_lxhotkey);
+#ifdef WITH_GTK
+    if (do_gui)
+        register_gui_plugin("gtk", &fm_module_init_lxhotkey_gui);
+#else
+    (void)do_gui;
+#endif
 }
 
 
@@ -478,16 +504,10 @@ int main(int argc, char *argv[])
         cmd += 4;
     }
 
-    /* init LibFM and FmModule */
-    fm_init(NULL);
-    fm_modules_add_directory(PACKAGE_PLUGINS_DIR);
-    fm_module_register_lxhotkey();
-    if (do_gui)
-        fm_module_register_lxhotkey_gui();
+    register_builtin_plugins(do_gui);
 
     LXKEYS_ERROR = g_quark_from_static_string("lxhotkey-error");
 
-    CHECK_MODULES();
     if (do_gui) /* load GUI plugin if requested */
         for (gui_plugin = gui_plugins; gui_plugin; gui_plugin = gui_plugin->next)
             if (g_ascii_strcasecmp(gui_plugin->name, cmd) == 0)
@@ -511,6 +531,10 @@ int main(int argc, char *argv[])
     for (plugin = plugins; plugin; plugin = plugin->next)
         if (g_ascii_strcasecmp(plugin->name, wm_name) == 0)
             break;
+    if (!plugin && g_ascii_strcasecmp(wm_name, "labwc") == 0)
+        for (plugin = plugins; plugin; plugin = plugin->next)
+            if (g_ascii_strcasecmp(plugin->name, "Openbox") == 0)
+                break;
     if (!plugin) {
         g_set_error(&error, LXKEYS_ERROR, LXKEYS_NOT_SUPPORTED,
                     _("Could not find a plugin for window manager %s."), wm_name);
@@ -687,9 +711,6 @@ _exit:
             fprintf(stderr, "LXHotkey: %s\n", error->message);
         g_error_free(error);
     }
-    fm_module_unregister_type("lxhotkey");
-    if (do_gui)
-        fm_module_unregister_type("lxhotkey_gui");
     while (plugins) {
         plugin = plugins;
         plugins = plugin->next;
diff --git a/src/lxhotkey.h b/src/lxhotkey.h
index b123c74..fbeced6 100644
--- a/src/lxhotkey.h
+++ b/src/lxhotkey.h
@@ -21,7 +21,7 @@
 #ifndef _LXKEYS_H_
 #define _LXKEYS_H_ 1
 
-#include <libfm/fm.h>
+#include <glib.h>
 
 G_BEGIN_DECLS
 
