[PATCH 1/4] Add keymap generation from the xkb database

Alexey Gladkov <[email protected]> Thu, 4 Jan 2024 18:15:15 +0000
Newsgroups dev.linux.lists.kbd
Message-ID <276e4450193ad26c5b3536d8421378629816edb2.1704392039.git.legion@kernel.org>
Xorg/Wayland uses its own keyboard configuration mechanism. Because of
this, the configuration of the text console and Xorg layouts may differ.
Not all combinations of Xorg layouts have a keymap in the kbd database.
It's inconvenient for users.

To solve this we can try converting the Xorg layout to a text console
keymap.

To access the XKB (X Keyboard Extension) database, you can use
libxkbcommon, which does not depend on the X11 libraries.

libxkbcommon is a keyboard keymap compiler and support library which
processes a reduced subset of keymaps as defined by the XKB
specification [1].

libxkbcommon does not distribute a keymap dataset itself, other than for
testing purposes. The most common dataset is xkeyboard-config, which is
used by all current distributions for their X11 XKB data [2].

There is a project that uses a similar approach to unify configurations.
This is console-setup [3].

I talked with Anton Zinoviev and he helped me figure out their idea of
switching multiple layouts with a single key combination:

> how you solved the problem of switching more than two layouts with one
> key combination ?

The keys with ISO_Next_Group, for example the right Alt (keynumber 100),
are defined like this:

keycode 100 = ShiftL_lock
ShiftL keycode 100 = ShiftR_lock
ShiftR keycode 100 = ShiftR_lock
ShiftL ShiftR keycode 100 = ShiftL_lock

So this key will work in the following way:

no modifiers are active -> ShiftL is active
ShiftL is active -> ShiftL and ShiftR are active
ShiftL and ShiftR are active -> ShiftR is active
ShiftR is active -> no modifiers are active

The groups (the layouts) are placed in the following way:

active modifiers  1 group    2 groups    3 groups    4 groups
----------------  -------    --------    --------    --------
no modifier       group 1    group 1     group 1     group 1
ShiftL            group 1    group 2     group 2     group 2
ShiftR            group 1    group 2     group 3     group 4
ShiftL+ShiftR     group 1    group 1     group 1     group 3

In this way we have the following cycles:

1 group: gr1->gr1->gr1->...
2 groups: gr1->gr2->gr1->gr2->...
3 groups: gr1->gr2->gr1->gr4->gr1->gr2->gr1->gr4->...
4 groups: gr1->gr2->gr3->gr4->gr1->gr2->gr3->gr4->...

--
Anton Zinoviev

The difference with this implementation is that I am not trying to read
and parse the XKB base files. I analyze the resulting xkb keymap.

[1] https://xkbcommon.org/doc/current/index.html
[2] https://www.freedesktop.org/wiki/Software/XKeyboardConfig/
[3] https://salsa.debian.org/installer-team/console-setup.git

Signed-off-by: Alexey Gladkov <[email protected]>
---
 configure.ac     |  10 +
 src/Makefile.am  |   6 +
 src/loadkeys.c   |  64 +++++-
 src/loadkeys.h   |  13 ++
 src/xkbsupport.c | 513 +++++++++++++++++++++++++++++++++++++++++++++++
 src/xkbsupport.h |  16 ++
 6 files changed, 613 insertions(+), 9 deletions(-)
 create mode 100644 src/loadkeys.h
 create mode 100644 src/xkbsupport.c
 create mode 100644 src/xkbsupport.h

diff --git a/configure.ac b/configure.ac
index a4be8383..f57317e5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -179,6 +179,15 @@ AM_CONDITIONAL(BUILD_TESTS, test "$build_tests" != "no")
 AC_CHECK_PROG([HAVE_DOXYGEN], [doxygen], [yes], [no])
 AM_CONDITIONAL(HAVE_DOXYGEN, test "$HAVE_DOXYGEN" = "yes")
 
+AC_ARG_ENABLE([xkb],
+	[AS_HELP_STRING([--enable-xkb],
+			[allow to generate kernel keymaps based on xkb database @<:@default=yes@:>@])],
+	[USE_XKB=$enableval],[USE_XKB=yes])
+AS_IF([test "x$USE_XKB" != xno],
+	[PKG_CHECK_MODULES(XKBCOMMON, xkbcommon, [HAVE_XKBCOMMON=yes], [HAVE_XKBCOMMON=no])],
+	[USE_XKB=no])
+AM_CONDITIONAL(USE_XKB, test "$USE_XKB" = "yes")
+
 AC_CONFIG_FILES([Makefile
 		data/Makefile
 		docs/Makefile
@@ -228,4 +237,5 @@ AC_MSG_RESULT([
 	libkbdfile:             ${BUILD_LIBKBDFILE}
 	libkeymap:              ${BUILD_LIBKEYMAP}
 	libkfont:               ${BUILD_LIBKFONT}
+	xkb support:            ${USE_XKB}
 ])
diff --git a/src/Makefile.am b/src/Makefile.am
index c495883d..7e4757e5 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -78,6 +78,12 @@ LDADD = \
 	@LIBINTL@ \
 	$(CODE_COVERAGE_LIBS)
 
+if USE_XKB
+loadkeys_SOURCES += xkbsupport.c
+loadkeys_CFLAGS = -DUSE_XKB @XKBCOMMON_CFLAGS@
+LDADD += @XKBCOMMON_LIBS@
+endif
+
 install-exec-hook:
 	for i in psfaddtable psfgettable psfstriptable; do \
 		rm -f $(DESTDIR)$(bindir)/$$i; \
diff --git a/src/loadkeys.c b/src/loadkeys.c
index 0176c518..d0178c77 100644
--- a/src/loadkeys.c
+++ b/src/loadkeys.c
@@ -25,6 +25,12 @@
 #include "paths.h"
 #include "keymap.h"
 
+#include "loadkeys.h"
+
+#ifdef USE_XKB
+#include "xkbsupport.h"
+#endif
+
 static const char *const dirpath1[] = {
 	DATADIR "/" KEYMAPDIR "/**",
 	KERNDIR "/",
@@ -90,6 +96,16 @@ int main(int argc, char *argv[])
 	struct kbdfile_ctx *fctx;
 	struct kbdfile *fp = NULL;
 
+#ifdef USE_XKB
+	struct xkeymap_params xkeymap_params = {
+		.model = "pc104",
+		.layout = NULL,
+		.variant = NULL,
+		.options = NULL
+	};
+	int use_xkb = 0;
+#endif
+
 	set_progname(argv[0]);
 	setuplocale();
 
@@ -108,9 +124,22 @@ int main(int argc, char *argv[])
 		{ "quiet", no_argument, NULL, 'q' },
 		{ "verbose", no_argument, NULL, 'v' },
 		{ "version", no_argument, NULL, 'V' },
+#ifdef USE_XKB
+		//{ "xkb-rules", required_argument, NULL, 1 },
+		{ "xkb-model", required_argument, NULL, 2 },
+		{ "xkb-layout", required_argument, NULL, 3 },
+		{ "xkb-variant", required_argument, NULL, 4 },
+		{ "xkb-options", required_argument, NULL, 5 },
+#endif
 		{ NULL, 0, NULL, 0 }
 	};
 	const struct kbd_help opthelp[] = {
+#ifdef USE_XKB
+		{ "--xkb-model=STR",    _("Specifies model used to choose component names.") },
+		{ "--xkb-layout=STR",   _("Specifies layout used to choose component names.") },
+		{ "--xkb-variant=STR",  _("Specifies layout variant used to choose component names.") },
+		{ "--xkb-options=STR",  _("Adds an option used to choose component names.") },
+#endif
 		{ "-C, --console=DEV",  _("the console device to be used.") },
 		{ "-a, --ascii",        _("force conversion to ASCII.") },
 		{ "-b, --bkeymap",      _("output a binary keymap to stdout.") },
@@ -127,15 +156,6 @@ int main(int argc, char *argv[])
 		{ NULL, NULL }
 	};
 
-	enum options {
-		OPT_A = (1 << 1),
-		OPT_B = (1 << 2),
-		OPT_D = (1 << 3),
-		OPT_M = (1 << 4),
-		OPT_U = (1 << 5),
-		OPT_P = (1 << 6)
-	};
-
 	ctx = lk_init();
 	if (!ctx) {
 		exit(EXIT_FAILURE);
@@ -146,6 +166,24 @@ int main(int argc, char *argv[])
 
 	while ((c = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) {
 		switch (c) {
+#ifdef USE_XKB
+			case 2:
+				xkeymap_params.model = optarg;
+				use_xkb = 1;
+				break;
+			case 3:
+				xkeymap_params.layout = optarg;
+				use_xkb = 1;
+				break;
+			case 4:
+				xkeymap_params.variant = optarg;
+				use_xkb = 1;
+				break;
+			case 5:
+				xkeymap_params.options = optarg;
+				use_xkb = 1;
+				break;
+#endif
 			case 'a':
 				options |= OPT_A;
 				break;
@@ -249,6 +287,14 @@ int main(int argc, char *argv[])
 		if (rc == -1)
 			goto fail;
 
+#ifdef USE_XKB
+	} else if (use_xkb) {
+		rc = convert_xkb_keymap(ctx, &xkeymap_params, options);
+
+		if (rc == -1)
+			goto fail;
+
+#endif
 	} else if (optind == argc) {
 		if (!(fp = kbdfile_new(fctx)))
 			kbd_error(EXIT_FAILURE, 0, _("Unable to create kbdfile instance: %m"));
diff --git a/src/loadkeys.h b/src/loadkeys.h
new file mode 100644
index 00000000..d6ff6e7c
--- /dev/null
+++ b/src/loadkeys.h
@@ -0,0 +1,13 @@
+#ifndef __LOADKEYS_H__
+#define __LOADKEYS_H__
+
+enum options {
+	OPT_A = (1 << 1),
+	OPT_B = (1 << 2),
+	OPT_D = (1 << 3),
+	OPT_M = (1 << 4),
+	OPT_U = (1 << 5),
+	OPT_P = (1 << 6)
+};
+
+#endif /* __LOADKEYS_H__ */
diff --git a/src/xkbsupport.c b/src/xkbsupport.c
new file mode 100644
index 00000000..694d516f
--- /dev/null
+++ b/src/xkbsupport.c
@@ -0,0 +1,513 @@
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+
+#include "libcommon.h"
+#include "keymap.h"
+#include "loadkeys.h"
+#include "xkbsupport.h"
+
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+
+static const int layout_switch[] = {
+	0,
+	(1 << KG_SHIFTL),
+	(1 << KG_SHIFTR),
+	(1 << KG_SHIFTL) | (1 << KG_SHIFTR)
+};
+
+/*
+ * number    |
+ * of groups | group
+ * --------------------------
+ *         0 | { 0, 0, 0, 0 }
+ *         1 | { 0, 1, 1, 0 }
+ *         2 | { 0, 1, 2, 0 }
+ *         3 | { 0, 1, 3, 2 }
+ */
+static const unsigned int layouts[4][4] = {
+	{ 0, 0, 0, 0 },
+	{ 0, 1, 1, 0 },
+	{ 0, 1, 2, 0 },
+	{ 0, 1, 3, 2 },
+};
+
+struct xkeymap {
+	struct xkb_context *xkb;
+	struct xkb_keymap *keymap;
+	struct lk_ctx *ctx;
+};
+
+/*
+ * Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
+ * keycode set (where ESC is 9).
+ */
+#define EVDEV_OFFSET 8
+
+/*
+ * Convert xkb keycode to kernel keycode.
+ */
+#define KERN_KEYCODE(keycode) (keycode - EVDEV_OFFSET)
+
+/*
+ * Copied from libxkbcommon.
+ * Don't allow more modifiers than we can hold in xkb_mod_mask_t.
+ */
+#define XKB_MAX_MODS ((xkb_mod_index_t)(sizeof(xkb_mod_mask_t) * 8))
+
+struct xkb_mask {
+	xkb_mod_mask_t mask[XKB_MAX_MODS];
+	size_t num;
+};
+
+struct modifier_mapping {
+	const char *xkb_mod;
+	const char *krn_mod;
+	const int bit;
+};
+
+static struct modifier_mapping modifier_mapping[] = {
+	{ "Shift",    "shift",   (1u << KG_SHIFT) },
+	{ "Control",  "control", (1u << KG_CTRL)  },
+	{ "Lock",     "???",     0                },
+	{ "Mod1",     "alt",     (1u << KG_ALT)   },
+	{ "Mod2",     "???",     0                },
+	{ "Mod3",     "???",     0                },
+	{ "Mod4",     "???",     0                },
+	{ "Mod5",     "altgr",   (1u << KG_ALT)   },
+	{ NULL, NULL, 0 },
+};
+
+static struct modifier_mapping *convert_modifier(const char *xkb_name)
+{
+	struct modifier_mapping *map = modifier_mapping;
+
+	while (map->xkb_mod) {
+		if (!strcasecmp(map->xkb_mod, xkb_name))
+			return map;
+		map++;
+	}
+
+	return NULL;
+}
+
+struct symbols_mapping {
+	const char *xkb_sym;
+	const char *krn_sym;
+};
+
+static struct symbols_mapping symbols_mapping[] = {
+	{ "0", "zero" },
+	{ "1", "one" },
+	{ "2", "two" },
+	{ "3", "three" },
+	{ "4", "four" },
+	{ "5", "five" },
+	{ "6", "six" },
+	{ "7", "seven" },
+	{ "8", "eight" },
+	{ "9", "nine" },
+	{ "KP_Insert", "KP_0" },
+	{ "KP_End", "KP_1" },
+	{ "KP_Down", "KP_2" },
+	{ "KP_Next", "KP_3" },
+	{ "KP_Left", "KP_4" },
+	{ "KP_Right", "KP_6" },
+	{ "KP_Home", "KP_7" },
+	{ "KP_Up", "KP_8" },
+	{ "KP_Prior", "KP_9" },
+	{ "KP_Begin", "VoidSymbol" },
+	{ "KP_Delete", "VoidSymbol" },
+	{ "Alt_R", "Alt" },
+	{ "Alt_L", "Alt" },
+	{ "Control_R", "Control" },
+	{ "Control_L", "Control" },
+	{ "Super_R", "Alt" },
+	{ "Super_L", "Alt" },
+	{ "Hyper_R", "Alt" },
+	{ "Hyper_L", "Alt" },
+	{ "Mode_switch", "AltGr" },
+	{ "ISO_Group_Shift", "AltGr" },
+	{ "ISO_Group_Latch", "AltGr" },
+	{ "ISO_Group_Lock", "AltGr_Lock" },
+	{ "ISO_Next_Group", "AltGr_Lock" },
+	{ "ISO_Next_Group_Lock", "AltGr_Lock" },
+	{ "ISO_Prev_Group", "AltGr_Lock" },
+	{ "ISO_Prev_Group_Lock", "AltGr_Lock" },
+	{ "ISO_First_Group", "AltGr_Lock" },
+	{ "ISO_First_Group_Lock", "AltGr_Lock" },
+	{ "ISO_Last_Group", "AltGr_Lock" },
+	{ "ISO_Last_Group_Lock", "AltGr_Lock" },
+	{ "ISO_Level3_Shift", "AltGr" },
+	{ "ISO_Left_Tab", "Meta_Tab" },
+	{ "XF86Switch_VT_1", "Console_1" },
+	{ "XF86Switch_VT_2", "Console_2" },
+	{ "XF86Switch_VT_3", "Console_3" },
+	{ "XF86Switch_VT_4", "Console_4" },
+	{ "XF86Switch_VT_5", "Console_5" },
+	{ "XF86Switch_VT_6", "Console_6" },
+	{ "XF86Switch_VT_7", "Console_7" },
+	{ "XF86Switch_VT_8", "Console_8" },
+	{ "XF86Switch_VT_9", "Console_9" },
+	{ "XF86Switch_VT_10", "Console_10" },
+	{ "XF86Switch_VT_11", "Console_11" },
+	{ "XF86Switch_VT_12", "Console_12" },
+	{ "Sys_Req", "Last_Console" },
+	{ "Print", "Control_backslash" },
+	{ NULL, NULL },
+};
+
+static const char *map_xkbsym_to_ksym(const char *xkb_sym)
+{
+	struct symbols_mapping *map = symbols_mapping;
+
+	while (map->xkb_sym) {
+		if (!strcmp(map->xkb_sym, xkb_sym))
+			return map->krn_sym;
+		map++;
+	}
+
+	return NULL;
+}
+
+static void print_modifiers(struct xkb_keymap *keymap, struct xkb_mask *mask)
+{
+	xkb_mod_index_t num_mods = xkb_keymap_num_mods(keymap);
+
+	for (size_t m = 0; m < mask->num; m++) {
+		for (xkb_mod_index_t mod = 0; mod < num_mods; mod++) {
+			if ((mask->mask[m] & (1u << mod)) == 0)
+				continue;
+
+			const char *modname = xkb_keymap_mod_get_name(keymap, mod);
+
+			printf("%ld:%s(%d) ", m, modname, mod);
+		}
+	}
+}
+
+static void xkeymap_keycode_mask(struct xkb_keymap *keymap,
+                                 xkb_layout_index_t layout, xkb_level_index_t level, xkb_keycode_t keycode,
+                                 struct xkb_mask *out)
+{
+	memset(out->mask, 0, sizeof(out->mask));
+	out->num = xkb_keymap_key_get_mods_for_level(keymap, keycode, layout, level, out->mask, ARRAY_SIZE(out->mask));
+}
+
+static void xkeymap_walk_printer(struct xkeymap *xkeymap,
+                                 xkb_layout_index_t layout, xkb_level_index_t level,
+				 xkb_keycode_t keycode, xkb_keysym_t sym)
+{
+	switch (sym) {
+		case XKB_KEY_ISO_Next_Group:
+			printf("* ");
+			break;
+		default:
+			printf("  ");
+			break;
+	}
+
+	printf("{%-12s} %d ", xkb_keymap_layout_get_name(xkeymap->keymap, layout), layout);
+	printf("%d ", level);
+
+	char s[255];
+	const char *symname = NULL;
+	int ret, kunicode;
+	uint32_t unicode = xkb_keysym_to_utf32(sym);
+
+	ret = xkb_keysym_get_name(sym, s, sizeof(s));
+
+	if (ret < 0 || (size_t)ret >= sizeof(s)) {
+		kbd_warning(0, "failed to get name of keysym");
+		return;
+	}
+
+	if (!(symname = map_xkbsym_to_ksym(s)))
+		symname = s;
+
+	if (unicode == 0) {
+		if ((kunicode = lk_ksym_to_unicode(xkeymap->ctx, symname)) < 0) {
+			printf("keycode %3d = %s", KERN_KEYCODE(keycode), symname);
+		} else {
+			unicode = (uint32_t) kunicode;
+		}
+	}
+
+	if (unicode > 0)
+		printf("keycode %3d = U+%04x %-32s", KERN_KEYCODE(keycode), unicode, symname);
+
+	printf(" ( ");
+
+	struct xkb_mask keycode_mask;
+	xkeymap_keycode_mask(xkeymap->keymap, layout, level, keycode, &keycode_mask);
+
+	if (keycode_mask.num > 0) {
+		xkb_mod_index_t num_mods = xkb_keymap_num_mods(xkeymap->keymap);
+
+		for (xkb_mod_index_t mod = 0; mod < num_mods; mod++) {
+			if ((keycode_mask.mask[0] & (1u << mod)) == 0)
+				continue;
+
+			const struct modifier_mapping *map = convert_modifier(xkb_keymap_mod_get_name(xkeymap->keymap, mod));
+
+			printf("%s ", map->krn_mod);
+		}
+	}
+
+	printf(") ( ");
+	print_modifiers(xkeymap->keymap, &keycode_mask);
+	printf(")\n");
+}
+
+static int parse_hexcode(const char *symname)
+{
+	int unicode;
+	size_t ret = strlen(symname);
+
+	if (ret >= 4 && symname[0] == '0' && symname[1] == 'x') {
+		errno = 0;
+		unicode = (int) strtol(symname + 2, NULL, 16);
+		if (errno == ERANGE) {
+			kbd_warning(0, "unable to convert unnamed non-Unicode xkb symbol `%s'", symname);
+			return -1;
+		}
+		return unicode;
+	}
+
+	if (ret >= 5 && symname[0] == 'U' &&
+	   isxdigit(symname[1]) && isxdigit(symname[2]) &&
+	   isxdigit(symname[3]) && isxdigit(symname[4])) {
+		errno = 0;
+		unicode = (int) strtol(symname + 1, NULL, 16);
+		if (errno == ERANGE) {
+			kbd_warning(0, "unable to convert unnamed unicode xkb symbol `%s'", symname);
+			return -1;
+		}
+		return unicode;
+	}
+
+	return 0;
+}
+
+static int xkeymap_get_code(struct lk_ctx *ctx, xkb_keysym_t symbol)
+{
+	uint32_t xkb_unicode;
+	int ret, unicode;
+	const char *symname;
+	char symbuf[BUFSIZ];
+
+	symbuf[0] = 0;
+
+	ret = xkb_keysym_get_name(symbol, symbuf, sizeof(symbuf));
+
+	if (ret < 0 || (size_t)ret >= sizeof(symbuf)) {
+		kbd_warning(0, "failed to get name of keysym");
+		return K_HOLE;
+	}
+
+	if ((unicode = parse_hexcode(symbuf)) != 0) {
+		;
+	} else if ((symname = map_xkbsym_to_ksym(symbuf)) != NULL) {
+		unicode = parse_hexcode(symname);
+		if (!unicode)
+			unicode = lk_ksym_to_unicode(ctx, symname);
+
+	} else if ((xkb_unicode = xkb_keysym_to_utf32(symbol)) > 0) {
+		unicode = (int) xkb_unicode;
+	} else {
+		unicode = K_HOLE;
+	}
+
+	ret = CODE_FOR_UNKNOWN_KSYM;
+
+	if (unicode > 0)
+		ret = lk_convert_code(ctx, unicode, TO_AUTO);
+
+	if (ret == CODE_FOR_UNKNOWN_KSYM) {
+		kbd_warning(0, "unable to convert code <%s> and replace it with VoidSymbol", symbuf);
+		ret = K_HOLE;
+	}
+
+	return ret;
+}
+
+static int xkeymap_get_symbol(struct xkb_keymap *keymap,
+		xkb_keycode_t keycode, xkb_layout_index_t layout, xkb_level_index_t level,
+		xkb_keysym_t *symbol)
+{
+	int num_syms;
+	const xkb_keysym_t *syms = NULL;
+
+	if (!(num_syms = xkb_keymap_key_get_syms_by_level(keymap, keycode, layout, level, &syms)))
+		return 0;
+
+	if (num_syms > 1) {
+		kbd_warning(0, "fixme: more %d symbol on level", num_syms);
+		return -1;
+	}
+
+	*symbol = syms[0];
+	return 1;
+}
+
+static void xkeymap_add_value(struct xkeymap *xkeymap, int modifier, int code, int keyvalue[MAX_NR_KEYMAPS])
+{
+	if (!modifier || (modifier & (1 << KG_SHIFT)))
+		code = lk_add_capslock(xkeymap->ctx, code);
+
+	keyvalue[modifier] = code;
+}
+
+static int xkeymap_walk(struct xkeymap *xkeymap)
+{
+	struct xkb_mask keycode_mask;
+	xkb_mod_index_t num_mods = xkb_keymap_num_mods(xkeymap->keymap);
+	xkb_keycode_t min_keycode = xkb_keymap_min_keycode(xkeymap->keymap);
+	xkb_keycode_t max_keycode = xkb_keymap_max_keycode(xkeymap->keymap);
+
+	if (KERN_KEYCODE(min_keycode) >= NR_KEYS) {
+		kbd_warning(0, "keymap defines more keycodes than the kernel can handle.");
+		min_keycode = (NR_KEYS - 1) + EVDEV_OFFSET;
+	}
+
+	if (KERN_KEYCODE(max_keycode) >= NR_KEYS) {
+		kbd_warning(0, "keymap defines more keycodes than the kernel can handle.");
+		max_keycode = (NR_KEYS - 1) + EVDEV_OFFSET;
+	}
+
+	xkb_layout_index_t num_layouts = xkb_keymap_num_layouts(xkeymap->keymap);
+
+	for (xkb_keycode_t keycode = min_keycode; keycode <= max_keycode; keycode++) {
+		int keyvalue[MAX_NR_KEYMAPS];
+
+		memset(keyvalue, 0, sizeof(keyvalue));
+
+		for (xkb_layout_index_t layout = 0; layout < num_layouts; layout++) {
+			xkb_level_index_t num_levels = xkb_keymap_num_levels_for_key(xkeymap->keymap, keycode, layout);
+
+			for (xkb_level_index_t level = 0; level < num_levels; level++) {
+				xkb_keysym_t sym;
+				int ret, value;
+
+				if (!(ret = xkeymap_get_symbol(xkeymap->keymap, keycode, layout, level, &sym)))
+					continue;
+				else if (ret < 0)
+					goto err;
+
+				if (getenv("LK_XKB_DEBUG")) {
+					xkeymap_walk_printer(xkeymap, layout, level, keycode, sym);
+					continue;
+				}
+
+				if (sym == XKB_KEY_ISO_Next_Group) {
+					int shiftl_lock = lk_ksym_to_unicode(xkeymap->ctx, "ShiftL_Lock");
+					int shiftr_lock = lk_ksym_to_unicode(xkeymap->ctx, "ShiftR_Lock");
+
+					xkeymap_add_value(xkeymap, layout_switch[0], shiftl_lock, keyvalue);
+					xkeymap_add_value(xkeymap, layout_switch[1], shiftr_lock, keyvalue);
+					xkeymap_add_value(xkeymap, layout_switch[2], shiftr_lock, keyvalue);
+					xkeymap_add_value(xkeymap, layout_switch[3], shiftl_lock, keyvalue);
+
+					goto process_keycode;
+				}
+
+				if ((value = xkeymap_get_code(xkeymap->ctx, sym)) < 0)
+					continue;
+
+				xkeymap_keycode_mask(xkeymap->keymap, layout, level, keycode, &keycode_mask);
+
+				for (unsigned short i = 0; i < ARRAY_SIZE(layout_switch); i++) {
+					const struct modifier_mapping *map;
+					int modifier = layout_switch[i];
+
+					if (layout != layouts[num_layouts - 1][i])
+						continue;
+
+					for (xkb_mod_index_t mod = 0; mod < num_mods; mod++) {
+						if (!(keycode_mask.mask[0] & (1u << mod)))
+							continue;
+
+						map = convert_modifier(xkb_keymap_mod_get_name(xkeymap->keymap, mod));
+						if (map)
+							modifier |= map->bit;
+					}
+
+					xkeymap_add_value(xkeymap, modifier, value, keyvalue);
+				}
+			}
+		}
+
+		if (getenv("LK_XKB_DEBUG"))
+			continue;
+
+process_keycode:
+		unsigned short layout = 0;
+
+		for (unsigned short i = 0; i < ARRAY_SIZE(keyvalue); i++) {
+			if (layout < ARRAY_SIZE(layout_switch) && i == layout_switch[layout + 1])
+				layout++;
+
+			if (!keyvalue[i]) {
+				if ((i & (1 << KG_SHIFT)))
+					keyvalue[i] = keyvalue[layout_switch[layout] | (1 << KG_SHIFT)];
+				if (!keyvalue[i])
+					keyvalue[i] = keyvalue[layout_switch[layout]];
+			}
+
+			if (lk_add_key(xkeymap->ctx, i, (int) KERN_KEYCODE(keycode), keyvalue[i]) < 0)
+				goto err;
+		}
+	}
+
+	return 0;
+err:
+	return -1;
+}
+
+
+int convert_xkb_keymap(struct lk_ctx *ctx, struct xkeymap_params *params, int options)
+{
+	struct xkeymap xkeymap;
+	int ret = -1;
+
+	struct xkb_rule_names names = {
+		.rules = "evdev",
+		.model = params->model,
+		.layout = params->layout,
+		.variant = params->variant,
+		.options = params->options,
+	};
+
+	xkeymap.ctx = ctx;
+
+	lk_set_keywords(ctx, LK_KEYWORD_ALTISMETA | LK_KEYWORD_STRASUSUAL);
+
+	xkeymap.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+	if (!xkeymap.xkb) {
+		kbd_warning(0, "xkb_context_new failed");
+		goto end;
+	}
+
+	xkeymap.keymap = xkb_keymap_new_from_names(xkeymap.xkb, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
+	if (!xkeymap.keymap) {
+		kbd_warning(0, "xkb_keymap_new_from_names failed");
+		goto end;
+	}
+
+	if (xkb_keymap_num_layouts(xkeymap.keymap) > ARRAY_SIZE(layout_switch)) {
+		kbd_warning(0, "too many layouts specified. At the moment, you can use no more than %ld", ARRAY_SIZE(layout_switch));
+		goto end;
+	}
+
+	if (!(ret = xkeymap_walk(&xkeymap))) {
+		if (options & OPT_P)
+			lk_dump_keymap(ctx, stdout, LK_SHAPE_SEPARATE_LINES, 0);
+	}
+
+end:
+	xkb_keymap_unref(xkeymap.keymap);
+	xkb_context_unref(xkeymap.xkb);
+
+	return ret;
+}
diff --git a/src/xkbsupport.h b/src/xkbsupport.h
new file mode 100644
index 00000000..e12e39f6
--- /dev/null
+++ b/src/xkbsupport.h
@@ -0,0 +1,16 @@
+#ifndef _XKB_SUPPORT_H_
+#define _XKB_SUPPORT_H_
+
+#include <xkbcommon/xkbcommon.h>
+#include "keymap.h"
+
+struct xkeymap_params {
+	const char *model;
+	const char *layout;
+	const char *variant;
+	const char *options;
+};
+
+int convert_xkb_keymap(struct lk_ctx *ctx, struct xkeymap_params *params, int options);
+
+#endif /* _XKB_SUPPORT_H_ */
-- 
2.43.0