Add new state API
authorDaniel Stone <daniel@fooishbar.org>
Wed, 21 Mar 2012 02:20:07 +0000 (02:20 +0000)
committerDaniel Stone <daniel@fooishbar.org>
Wed, 21 Mar 2012 02:22:04 +0000 (02:22 +0000)
Add new API to deal with xkb_state objects, including
xkb_state_update_key, which runs the XKB action machinery internally to
calculate what exactly happens to the state when a given key is pressed
or released.

The canonical way to deal with keys is now:
    struct xkb_state *state = xkb_state_new(xkb);
    xkb_keysym_t *syms;
    int num_syms;

    xkb_state_update_key(state, key, is_down);
    num_syms = xkb_key_get_syms(state, key, &syms);

More state handling API, including a way to get at or ignore preserved
modifiers, is on its way.

Signed-off-by: Daniel Stone <daniel@fooishbar.org>
include/xkbcommon/xkbcommon.h
src/Makefile.am
src/XKBcommonint.h
src/map.c
src/state.c [new file with mode: 0644]
test/Makefile.am
test/state.c [new file with mode: 0644]

index 2092680..9507d15 100644 (file)
@@ -490,6 +490,11 @@ struct xkb_state {
        unsigned char   compat_lookup_mods; /* effective mods + group */
 
        unsigned short  ptr_buttons; /* core pointer buttons */
+
+        int refcnt;
+        void *filters;
+        int num_filters;
+        struct xkb_desc *xkb;
 };
 
 #define        XkbStateFieldFromRec(s) XkbBuildCoreState((s)->lookup_mods,(s)->group)
@@ -551,8 +556,42 @@ _X_EXPORT extern xkb_keysym_t
 xkb_string_to_keysym(const char *s);
 
 _X_EXPORT unsigned int
-xkb_key_get_syms(struct xkb_desc *xkb, struct xkb_state *state,
-                 xkb_keycode_t key, xkb_keysym_t **syms_out);
+xkb_key_get_syms(struct xkb_state *state, xkb_keycode_t key,
+                 xkb_keysym_t **syms_out);
+
+/**
+ * @defgroup state XKB state objects
+ * Creation, destruction and manipulation of keyboard state objects, * representing modifier and group state.
+ *
+ * @{
+ */
+
+/**
+ * Allocates a new XKB state object for use with the given keymap.
+ */
+_X_EXPORT struct xkb_state *
+xkb_state_new(struct xkb_desc *xkb);
+
+/**
+ * Adds a reference to a state object, so it will not be freed until unref.
+ */
+_X_EXPORT void
+xkb_state_ref(struct xkb_state *state);
+
+/**
+ * Unrefs and potentially deallocates a state object; the caller must not
+ * use the state object after calling this.
+ */
+_X_EXPORT void
+xkb_state_unref(struct xkb_state *state);
+
+/**
+ * Updates a state object to reflect the given key being pressed or released.
+ */
+_X_EXPORT void
+xkb_state_update_key(struct xkb_state *state, xkb_keycode_t key, int down);
+
+/** @} */
 
 _XFUNCPROTOEND
 
index 5a4847a..ef66913 100644 (file)
@@ -15,6 +15,7 @@ libxkbcommon_la_SOURCES = \
        map.c \
        maprules.c \
        misc.c \
+       state.c \
        text.c \
        xkb.c \
        xkballoc.h \
index 3f83277..d09060b 100644 (file)
@@ -86,4 +86,10 @@ authorization from the authors.
 #define        XkmLegalSection(m)      (((m)&(~XkmKeymapLegal))==0)
 #define        XkmSingleSection(m)     (XkmLegalSection(m)&&(((m)&(~(m)+1))==(m)))
 
+extern unsigned int xkb_key_get_group(struct xkb_state *state,
+                                      xkb_keycode_t key);
+extern unsigned int xkb_key_get_level(struct xkb_state *state,
+                                      xkb_keycode_t key,
+                                      unsigned int group);
+
 #endif /* _XKBCOMMONINT_H_ */
index e76d0bc..d5d20d7 100644 (file)
--- a/src/map.c
+++ b/src/map.c
 /**
  * Returns the level to use for the given key and state, or -1 if invalid.
  */
-static int
-xkb_key_get_level(struct xkb_desc *xkb, struct xkb_state *state,
-                  xkb_keycode_t key, unsigned int group)
+unsigned int
+xkb_key_get_level(struct xkb_state *state, xkb_keycode_t key,
+                  unsigned int group)
 {
-    struct xkb_key_type *type = XkbKeyType(xkb, key, group);
+    struct xkb_key_type *type = XkbKeyType(state->xkb, key, group);
     unsigned int active_mods = state->mods & type->mods.mask;
     int i;
 
@@ -80,18 +80,17 @@ xkb_key_get_level(struct xkb_desc *xkb, struct xkb_state *state,
 }
 
 /**
- * Returns the group to use for the given key and state, or -1 if invalid,
- * taking wrapping/clamping/etc into account.
+ * Returns the group to use for the given key and state, taking
+ * wrapping/clamping/etc into account.
  */
-static int
-xkb_key_get_group(struct xkb_desc *xkb, struct xkb_state *state,
-                  xkb_keycode_t key)
+unsigned int
+xkb_key_get_group(struct xkb_state *state, xkb_keycode_t key)
 {
-    unsigned int info = XkbKeyGroupInfo(xkb, key);
-    unsigned int num_groups = XkbKeyNumGroups(xkb, key);
+    unsigned int info = XkbKeyGroupInfo(state->xkb, key);
+    unsigned int num_groups = XkbKeyNumGroups(state->xkb, key);
     int ret = state->group;
 
-    if (ret < XkbKeyNumGroups(xkb, key))
+    if (ret < XkbKeyNumGroups(state->xkb, key))
         return ret;
 
     switch (XkbOutOfRangeGroupAction(info)) {
@@ -135,19 +134,20 @@ err:
  * number of symbols pointed to in syms_out.
  */
 unsigned int
-xkb_key_get_syms(struct xkb_desc *xkb, struct xkb_state *state,
-                 xkb_keycode_t key, xkb_keysym_t **syms_out)
+xkb_key_get_syms(struct xkb_state *state, xkb_keycode_t key,
+                 xkb_keysym_t **syms_out)
 {
+    struct xkb_desc *xkb = state->xkb;
     int group;
     int level;
 
-    if (!xkb || !state || key < xkb->min_key_code || key > xkb->max_key_code)
+    if (!state || key < xkb->min_key_code || key > xkb->max_key_code)
         return -1;
 
-    group = xkb_key_get_group(xkb, state, key);
+    group = xkb_key_get_group(state, key);
     if (group == -1)
         goto err;
-    level = xkb_key_get_level(xkb, state, key, group);
+    level = xkb_key_get_level(state, key, group);
     if (level == -1)
         goto err;
 
diff --git a/src/state.c b/src/state.c
new file mode 100644 (file)
index 0000000..f248d0f
--- /dev/null
@@ -0,0 +1,463 @@
+/************************************************************
+Copyright (c) 1993 by Silicon Graphics Computer Systems, Inc.
+
+Permission to use, copy, modify, and distribute this
+software and its documentation for any purpose and without
+fee is hereby granted, provided that the above copyright
+notice appear in all copies and that both that copyright
+notice and this permission notice appear in supporting
+documentation, and that the name of Silicon Graphics not be
+used in advertising or publicity pertaining to distribution
+of the software without specific prior written permission.
+Silicon Graphics makes no representation about the suitability
+of this software for any purpose. It is provided "as is"
+without any express or implied warranty.
+
+SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
+SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON
+GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
+DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION  WITH
+THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+********************************************************/
+
+/*
+ * Copyright © 2012 Intel Corporation
+ *
+ * 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 (including the next
+ * paragraph) 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.
+ *
+ * Author: Daniel Stone <daniel@fooishbar.org>
+ */
+
+/*
+ * This is a bastardised version of xkbActions.c from the X server which
+ * does not support, for the moment:
+ *   - AccessX sticky/debounce/etc (will come later)
+ *   - pointer keys (may come later)
+ *   - key redirects (unlikely)
+ *   - messages (very unlikely)
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <assert.h>
+
+#include "xkbcommon/xkbcommon.h"
+#include "XKBcommonint.h"
+
+struct xkb_filter {
+    struct xkb_state *state;
+    union xkb_action action;
+    xkb_keycode_t keycode;
+    uint32_t priv;
+    int (*func)(struct xkb_filter *filter, xkb_keycode_t key, int down);
+    int refcnt;
+    struct xkb_filter *next;
+};
+
+static union xkb_action *
+xkb_key_get_action(struct xkb_state *state, xkb_keycode_t key)
+{
+    int group, level;
+
+    if (!XkbKeyHasActions(state->xkb, key) ||
+        !XkbKeycodeInRange(state->xkb, key)) {
+        static union xkb_action fake;
+        memset(&fake, 0, sizeof(fake));
+        fake.type = XkbSA_NoAction;
+        return &fake;
+    }
+
+    group = xkb_key_get_group(state, key);
+    level = xkb_key_get_level(state, key, group);
+
+    return XkbKeyActionEntry(state->xkb, key, level, group);
+}
+
+static struct xkb_filter *
+xkb_filter_new(struct xkb_state *state)
+{
+    int i;
+    int old_size = state->num_filters;
+    struct xkb_filter *filters = state->filters;
+
+    for (i = 0; i < state->num_filters; i++) {
+        if (filters[i].func)
+            continue;
+        filters[i].state = state;
+        filters[i].refcnt = 1;
+        return &filters[i];
+    }
+
+    if (state->num_filters > 0)
+        state->num_filters *= 2;
+    else
+        state->num_filters = 4;
+    filters = realloc(filters, state->num_filters * sizeof(*filters));
+    if (!filters) { /* WSGO */
+        state->num_filters = old_size;
+        return NULL;
+    }
+    state->filters = filters;
+    memset(&filters[old_size], 0,
+           (state->num_filters - old_size) * sizeof(*filters));
+
+    filters[old_size].state = state;
+    filters[old_size].refcnt = 1;
+
+    return &filters[old_size];
+}
+
+/***====================================================================***/
+
+static int
+xkb_filter_group_set_func(struct xkb_filter *filter, xkb_keycode_t keycode,
+                          int down)
+{
+    if (keycode != filter->keycode) {
+        filter->action.group.flags &= ~XkbSA_ClearLocks;
+        return 1;
+    }
+
+    if (down) {
+        filter->refcnt++;
+        return 0;
+    }
+    else if (--filter->refcnt > 0) {
+        return 0;
+    }
+
+    if (filter->action.group.flags & XkbSA_GroupAbsolute)
+        filter->state->base_group = filter->action.group.group;
+    else
+        filter->state->base_group = -filter->action.group.group;
+    if (filter->action.group.flags & XkbSA_ClearLocks)
+        filter->state->locked_group = 0;
+
+    filter->func = NULL;
+
+    return 1;
+}
+
+static int
+xkb_filter_group_set_new(struct xkb_state *state, xkb_keycode_t keycode,
+                         union xkb_action *action)
+{
+    struct xkb_filter *filter = xkb_filter_new(state);
+
+    if (!filter) /* WSGO */
+        return -1;
+    filter->keycode = keycode;
+    filter->func = xkb_filter_group_set_func;
+    filter->action = *action;
+
+    if (action->group.flags & XkbSA_GroupAbsolute) {
+        filter->action.group.group = filter->state->base_group;
+        filter->state->base_group = action->group.group;
+    }
+    else {
+        filter->state->base_group += action->group.group;
+    }
+
+    return 1;
+}
+
+static int
+xkb_filter_mod_set_func(struct xkb_filter *filter, xkb_keycode_t keycode,
+                        int down)
+{
+    if (keycode != filter->keycode) {
+        filter->action.mods.flags &= ~XkbSA_ClearLocks;
+        return 1;
+    }
+
+    if (down) {
+        filter->refcnt++;
+        return 0;
+    }
+    else if (--filter->refcnt > 0) {
+        return 0;
+    }
+
+    filter->state->base_mods &= ~(filter->action.mods.mask);
+    if (filter->action.mods.flags & XkbSA_ClearLocks)
+        filter->state->locked_mods &= ~filter->action.mods.mask;
+
+    filter->func = NULL;
+
+    return 1;
+}
+
+static int
+xkb_filter_mod_set_new(struct xkb_state *state, xkb_keycode_t keycode,
+                       union xkb_action *action)
+{
+    struct xkb_filter *filter = xkb_filter_new(state);
+
+    if (!filter) /* WSGO */
+        return -1;
+    filter->keycode = keycode;
+    filter->func = xkb_filter_mod_set_func;
+    filter->action = *action;
+
+    filter->state->base_mods |= action->mods.mask;
+
+    return 1;
+}
+
+static int
+xkb_filter_mod_lock_func(struct xkb_filter *filter, xkb_keycode_t keycode,
+                         int down)
+{
+    if (keycode != filter->keycode)
+        return 1;
+
+    if (down) {
+        filter->refcnt++;
+        return 0;
+    }
+    if (--filter->refcnt > 0)
+        return 0;
+
+    filter->state->locked_mods &= ~filter->priv;
+    filter->func = NULL;
+    return 1;
+}
+
+static int
+xkb_filter_mod_lock_new(struct xkb_state *state, xkb_keycode_t keycode,
+                        union xkb_action *action)
+{
+    struct xkb_filter *filter = xkb_filter_new(state);
+
+    if (!filter) /* WSGO */
+        return 0;
+
+    filter->keycode = keycode;
+    filter->func = xkb_filter_mod_lock_func;
+    filter->action = *action;
+    filter->priv = state->locked_mods & action->mods.mask;
+    state->locked_mods |= action->mods.mask;
+
+    return 1;
+}
+
+enum xkb_key_latch_state {
+    NO_LATCH,
+    LATCH_KEY_DOWN,
+    LATCH_PENDING,
+};
+
+static int
+xkb_filter_mod_latch_func(struct xkb_filter *filter, xkb_keycode_t keycode,
+                          int down)
+{
+    enum xkb_key_latch_state latch = filter->priv;
+
+    if (down && latch == LATCH_PENDING) {
+        /* If this is a new keypress and we're awaiting our single latched
+         * keypress, then either break the latch if any random key is pressed,
+         * or promote it to a lock or plain base set if it's the same
+         * modifier. */
+        union xkb_action *action = xkb_key_get_action(filter->state, keycode);
+        if (action->type == XkbSA_LatchMods &&
+            action->mods.flags == filter->action.mods.flags &&
+            action->mods.mask == filter->action.mods.mask) {
+            filter->action = *action;
+            if (filter->action.mods.flags & XkbSA_LatchToLock) {
+                filter->action.type = XkbSA_LockMods;
+                filter->func = xkb_filter_mod_lock_func;
+                filter->state->locked_mods |= filter->action.mods.mask;
+            }
+            else {
+                filter->action.type = XkbSA_SetMods;
+                filter->func = xkb_filter_mod_set_func;
+                filter->state->base_mods |= filter->action.mods.mask;
+            }
+            filter->keycode = keycode;
+            filter->state->latched_mods &= ~filter->action.mods.mask;
+            /* XXX beep beep! */
+            return 0;
+        }
+        else if (((1 << action->type) & XkbSA_BreakLatch)) {
+            /* XXX: This may be totally broken, we might need to break the
+             *      latch in the next run after this press? */
+            filter->state->latched_mods &= ~filter->action.mods.mask;
+            filter->func = NULL;
+            return 1;
+        }
+    }
+    else if (!down && keycode == filter->keycode) {
+        /* Our key got released.  If we've set it to clear locks, and we
+         * currently have the same modifiers locked, then release them and
+         * don't actually latch.  Else we've actually hit the latching
+         * stage, so set PENDING and move our modifier from base to
+         * latched. */
+        if (latch == NO_LATCH ||
+            ((filter->action.mods.flags & XkbSA_ClearLocks) &&
+             (filter->state->locked_mods & filter->action.mods.mask) ==
+             filter->action.mods.mask)) {
+            /* XXX: We might be a bit overenthusiastic about clearing
+             *      mods other filters have set here? */
+            if (latch == LATCH_PENDING)
+                filter->state->latched_mods &= ~filter->action.mods.mask;
+            else
+                filter->state->base_mods &= ~filter->action.mods.mask;
+            filter->state->locked_mods &= ~filter->action.mods.mask;
+            filter->func = NULL;
+        }
+        else {
+            latch = LATCH_PENDING;
+            filter->state->base_mods &= ~filter->action.mods.mask;
+            filter->state->latched_mods |= filter->action.mods.mask;
+            /* XXX beep beep! */
+        }
+    }
+    else if (down && latch == LATCH_KEY_DOWN) {
+        /* Someone's pressed another key while we've still got the latching
+         * key held down, so keep the base modifier state active (from
+         * xkb_filter_mod_latch_new), but don't trip the latch, just clear
+         * it as soon as the modifier gets released. */
+        latch = NO_LATCH;
+    }
+
+    filter->priv = latch;
+
+    return 1;
+}
+
+static int
+xkb_filter_mod_latch_new(struct xkb_state *state, xkb_keycode_t keycode,
+                         union xkb_action *action)
+{
+    struct xkb_filter *filter = xkb_filter_new(state);
+    enum xkb_key_latch_state latch = LATCH_KEY_DOWN;
+
+    if (!filter) /* WSGO */
+        return -1;
+    filter->keycode = keycode;
+    filter->priv = latch;
+    filter->func = xkb_filter_mod_latch_func;
+    filter->action = *action;
+
+    filter->state->base_mods |= action->mods.mask;
+
+    return 1;
+}
+
+/**
+ * Applies any relevant filters to the key, first from the list of filters
+ * that are currently active, then if no filter has claimed the key, possibly
+ * apply a new filter from the key action.
+ */
+static void
+xkb_filter_apply_all(struct xkb_state *state, xkb_keycode_t key, int down)
+{
+    struct xkb_filter *filters = state->filters;
+    union xkb_action *act = NULL;
+    int send = 1;
+    int i;
+
+    /* First run through all the currently active filters and see if any of
+     * them have claimed this event. */
+    for (i = 0; i < state->num_filters; i++) {
+        if (!filters[i].func)
+            continue;
+        send &= (*filters[i].func)(&filters[i], key, down);
+    }
+
+    if (!send || !down)
+        return;
+
+    act = xkb_key_get_action(state, key);
+    switch (act->type) {
+    case XkbSA_SetMods:
+        send = xkb_filter_mod_set_new(state, key, act);
+        break;
+    case XkbSA_LatchMods:
+        send = xkb_filter_mod_latch_new(state, key, act);
+        break;
+    case XkbSA_LockMods:
+        send = xkb_filter_mod_lock_new(state, key, act);
+        break;
+    case XkbSA_SetGroup:
+        send = xkb_filter_group_set_new(state, key, act);
+        break;
+#if 0
+    case XkbSA_LatchGroup:
+        send = xkb_filter_mod_latch_new(state, key, act);
+        break;
+    case XkbSA_LockGroup:
+        send = xkb_filter_group_lock_new(state, key, act);
+        break;
+#endif
+    }
+
+    return;
+}
+
+struct xkb_state *
+xkb_state_new(struct xkb_desc *xkb)
+{
+    struct xkb_state *ret;
+    if (!xkb)
+        return NULL;
+
+    ret = calloc(sizeof(*ret), 1);
+    if (!ret)
+        return NULL;
+
+    ret->refcnt = 1;
+    ret->xkb = xkb;
+    return ret;
+}
+
+void
+xkb_state_unref(struct xkb_state *state)
+{
+    state->refcnt--;
+    assert(state->refcnt >= 0);
+    if (state->refcnt == 0)
+        return;
+    free(state);
+}
+
+/**
+ * Given a particular key event, updates the state structure to reflect the
+ * new modifiers.
+ */
+void
+xkb_state_update_key(struct xkb_state *state, xkb_keycode_t key, int down)
+{
+    xkb_filter_apply_all(state, key, down);
+
+    state->mods = (state->base_mods | state->latched_mods | state->locked_mods);
+    /* FIXME: Clamp/wrap locked_group */
+    state->group = state->locked_group + state->base_group +
+                   state->latched_group;
+    /* FIXME: Clamp/wrap effective group */
+
+    /* FIXME: Update LED state. */
+}
index 02ca6ee..44950aa 100644 (file)
@@ -4,7 +4,7 @@ LDADD = $(top_builddir)/src/libxkbcommon.la
 
 TESTS_ENVIRONMENT = $(SHELL)
 
-check_PROGRAMS = xkey filecomp namescomp rulescomp canonicalise
+check_PROGRAMS = xkey filecomp namescomp rulescomp canonicalise state
 TESTS = $(check_PROGRAMS:=.sh)
 
 EXTRA_DIST =           \
diff --git a/test/state.c b/test/state.c
new file mode 100644 (file)
index 0000000..54fc8ba
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Copyright © 2012 Intel Corporation
+ *
+ * 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 (including the next
+ * paragraph) 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.
+ *
+ * Author: Daniel Stone <daniel@fooishbar.org>
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <X11/X.h>
+#include <X11/Xdefs.h>
+#include <X11/keysym.h>
+#include <linux/input.h>
+#include "xkbcommon/xkbcommon.h"
+#include "xkbcomp/utils.h"
+#include "XKBcommonint.h"
+
+/* Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
+ * keycode set (where ESC is 9). */
+#define EVDEV_OFFSET 8
+
+int main(int argc, char *argv[])
+{
+    struct xkb_rule_names rmlvo;
+    struct xkb_desc *xkb;
+    struct xkb_state *state;
+    int num_syms;
+    xkb_keysym_t *syms;
+
+    rmlvo.rules = "evdev";
+    rmlvo.model = "pc104";
+    rmlvo.layout = "us";
+    rmlvo.variant = NULL;
+    rmlvo.options = NULL;
+
+    xkb = xkb_compile_keymap_from_rules(&rmlvo);
+
+    if (!xkb) {
+        fprintf(stderr, "Failed to compile keymap\n");
+        exit(1);
+    }
+
+    state = xkb_state_new(xkb);
+    assert(state->mods == 0);
+    assert(state->group == 0);
+
+    /* LCtrl down */
+    xkb_state_update_key(state, KEY_LEFTCTRL + EVDEV_OFFSET, 1);
+    assert(state->mods & ControlMask);
+
+    /* LCtrl + RAlt down */
+    xkb_state_update_key(state, KEY_RIGHTALT + EVDEV_OFFSET, 1);
+    assert(state->mods & Mod1Mask);
+    assert(state->locked_mods == 0);
+    assert(state->latched_mods == 0);
+
+    /* RAlt down */
+    xkb_state_update_key(state, KEY_LEFTCTRL + EVDEV_OFFSET, 0);
+    assert(!(state->mods & ControlMask) && (state->mods & Mod1Mask));
+
+    /* none down */
+    xkb_state_update_key(state, KEY_RIGHTALT + EVDEV_OFFSET, 0);
+    assert(state->mods == 0);
+    assert(state->group == 0);
+
+    /* Caps locked */
+    xkb_state_update_key(state, KEY_CAPSLOCK + EVDEV_OFFSET, 1);
+    xkb_state_update_key(state, KEY_CAPSLOCK + EVDEV_OFFSET, 0);
+    assert(state->mods & LockMask);
+    assert(state->mods == state->locked_mods);
+    num_syms = xkb_key_get_syms(state, KEY_Q + EVDEV_OFFSET, &syms);
+    assert(num_syms == 1 && syms[0] == XK_Q);
+
+    /* Caps unlocked */
+    xkb_state_update_key(state, KEY_CAPSLOCK + EVDEV_OFFSET, 1);
+    xkb_state_update_key(state, KEY_CAPSLOCK + EVDEV_OFFSET, 0);
+    assert(state->mods == 0);
+    num_syms = xkb_key_get_syms(state, KEY_Q + EVDEV_OFFSET, &syms);
+    assert(num_syms == 1 && syms[0] == XK_q);
+
+    xkb_state_unref(state);
+    xkb_free_keymap(xkb);
+
+    return 0;
+}