From ecea0d71b29d07c7909d196b2a0bae0cbb43c79d Mon Sep 17 00:00:00 2001 From: Daniel Stone Date: Wed, 21 Mar 2012 02:20:07 +0000 Subject: [PATCH] Add new state API 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 --- include/xkbcommon/xkbcommon.h | 43 +++- src/Makefile.am | 1 + src/XKBcommonint.h | 6 + src/map.c | 34 +-- src/state.c | 463 ++++++++++++++++++++++++++++++++++ test/Makefile.am | 2 +- test/state.c | 104 ++++++++ 7 files changed, 633 insertions(+), 20 deletions(-) create mode 100644 src/state.c create mode 100644 test/state.c diff --git a/include/xkbcommon/xkbcommon.h b/include/xkbcommon/xkbcommon.h index 2092680..9507d15 100644 --- a/include/xkbcommon/xkbcommon.h +++ b/include/xkbcommon/xkbcommon.h @@ -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 diff --git a/src/Makefile.am b/src/Makefile.am index 5a4847a..ef66913 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -15,6 +15,7 @@ libxkbcommon_la_SOURCES = \ map.c \ maprules.c \ misc.c \ + state.c \ text.c \ xkb.c \ xkballoc.h \ diff --git a/src/XKBcommonint.h b/src/XKBcommonint.h index 3f83277..d09060b 100644 --- a/src/XKBcommonint.h +++ b/src/XKBcommonint.h @@ -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_ */ diff --git a/src/map.c b/src/map.c index e76d0bc..d5d20d7 100644 --- a/src/map.c +++ b/src/map.c @@ -61,11 +61,11 @@ /** * 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 index 0000000..f248d0f --- /dev/null +++ b/src/state.c @@ -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 + */ + +/* + * 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 +#endif + +#include + +#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. */ +} diff --git a/test/Makefile.am b/test/Makefile.am index 02ca6ee..44950aa 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -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 index 0000000..54fc8ba --- /dev/null +++ b/test/state.c @@ -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 + */ + +#include +#include +#include +#include +#include +#include +#include +#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; +}