From e06cc7b07465369fb7c01c9778b84cf82c82fdcf Mon Sep 17 00:00:00 2001 From: David Herrmann Date: Wed, 27 Aug 2014 18:34:55 +0200 Subject: [PATCH] terminal: add xkb-based keyboard devices to idev The idev-keyboard object provides keyboard devices to the idev interface. It uses libxkbcommon to provide proper keymap support. So far, the keyboard implementation is pretty straightforward with one keyboard device per matching evdev element. We feed everything into the system keymap and provide proper high-level keyboard events to the application. Compose-features and IM need to be added later. --- Makefile.am | 1 + configure.ac | 2 +- src/libsystemd-terminal/idev-internal.h | 9 + src/libsystemd-terminal/idev-keyboard.c | 846 ++++++++++++++++++++++++++++++++ src/libsystemd-terminal/idev.c | 62 ++- src/libsystemd-terminal/idev.h | 49 ++ 6 files changed, 966 insertions(+), 3 deletions(-) create mode 100644 src/libsystemd-terminal/idev-keyboard.c diff --git a/Makefile.am b/Makefile.am index b51c522443..35a4c44a9f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2976,6 +2976,7 @@ libsystemd_terminal_la_SOURCES = \ src/libsystemd-terminal/idev-internal.h \ src/libsystemd-terminal/idev.c \ src/libsystemd-terminal/idev-evdev.c \ + src/libsystemd-terminal/idev-keyboard.c \ src/libsystemd-terminal/sysview.h \ src/libsystemd-terminal/sysview-internal.h \ src/libsystemd-terminal/sysview.c \ diff --git a/configure.ac b/configure.ac index 3900c4065b..a25ad3f2e2 100644 --- a/configure.ac +++ b/configure.ac @@ -1066,7 +1066,7 @@ AM_CONDITIONAL(ENABLE_MULTI_SEAT_X, [test "$have_multi_seat_x" = "yes"]) have_terminal=no AC_ARG_ENABLE(terminal, AS_HELP_STRING([--enable-terminal], [enable terminal support])) if test "x$enable_terminal" = "xyes"; then - PKG_CHECK_MODULES([TERMINAL], [ libevdev >= 1.2 ], [have_terminal=yes]) + PKG_CHECK_MODULES([TERMINAL], [ libevdev >= 1.2 xkbcommon >= 0.4 ], [have_terminal=yes]) AS_IF([test "x$have_terminal" != xyes -a "x$enable_terminal" = xyes], [AC_MSG_ERROR([*** terminal support requested but required dependencies not available])], [test "x$have_terminal" = xyes], diff --git a/src/libsystemd-terminal/idev-internal.h b/src/libsystemd-terminal/idev-internal.h index 3301ebf6e4..c416f4fadd 100644 --- a/src/libsystemd-terminal/idev-internal.h +++ b/src/libsystemd-terminal/idev-internal.h @@ -28,6 +28,7 @@ #include #include #include +#include #include "hashmap.h" #include "idev.h" #include "list.h" @@ -47,6 +48,14 @@ idev_element *idev_find_evdev(idev_session *s, dev_t devnum); int idev_evdev_new(idev_element **out, idev_session *s, struct udev_device *ud); /* + * Keyboard Devices + */ + +bool idev_is_keyboard(idev_device *d); +idev_device *idev_find_keyboard(idev_session *s, const char *name); +int idev_keyboard_new(idev_device **out, idev_session *s, const char *name); + +/* * Element Links */ diff --git a/src/libsystemd-terminal/idev-keyboard.c b/src/libsystemd-terminal/idev-keyboard.c new file mode 100644 index 0000000000..647aade932 --- /dev/null +++ b/src/libsystemd-terminal/idev-keyboard.c @@ -0,0 +1,846 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright (C) 2014 David Herrmann + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include +#include +#include +#include +#include +#include +#include "bus-util.h" +#include "hashmap.h" +#include "idev.h" +#include "idev-internal.h" +#include "macro.h" +#include "util.h" + +typedef struct kbdmap kbdmap; +typedef struct kbdctx kbdctx; +typedef struct idev_keyboard idev_keyboard; + +struct kbdmap { + unsigned long ref; + struct xkb_keymap *xkb_keymap; + xkb_mod_index_t modmap[IDEV_KBDMOD_CNT]; + xkb_led_index_t ledmap[IDEV_KBDLED_CNT]; +}; + +struct kbdctx { + unsigned long ref; + idev_context *context; + struct xkb_context *xkb_context; + struct kbdmap *kbdmap; + + sd_bus_slot *slot_locale_props_changed; + sd_bus_slot *slot_locale_get_all; + + char *locale_x11_model; + char *locale_x11_layout; + char *locale_x11_variant; + char *locale_x11_options; + char *last_x11_model; + char *last_x11_layout; + char *last_x11_variant; + char *last_x11_options; +}; + +struct idev_keyboard { + idev_device device; + kbdctx *kbdctx; + kbdmap *kbdmap; + + struct xkb_state *xkb_state; + + usec_t repeat_delay; + usec_t repeat_rate; + sd_event_source *repeat_timer; + + uint32_t n_syms; + idev_data evdata; + idev_data repdata; + + bool repeating : 1; +}; + +#define keyboard_from_device(_d) container_of((_d), idev_keyboard, device) + +#define KBDCTX_KEY "keyboard.context" /* hashmap key for global kbdctx */ +#define KBDXKB_SHIFT (8) /* xkb shifts evdev key-codes by 8 */ +#define KBDKEY_UP (0) /* KEY UP event value */ +#define KBDKEY_DOWN (1) /* KEY DOWN event value */ +#define KBDKEY_REPEAT (2) /* KEY REPEAT event value */ + +static const idev_device_vtable keyboard_vtable; + +static int keyboard_update_kbdmap(idev_keyboard *k); + +/* + * Keyboard Keymaps + */ + +static const char * const kbdmap_modmap[IDEV_KBDMOD_CNT] = { + [IDEV_KBDMOD_IDX_SHIFT] = XKB_MOD_NAME_SHIFT, + [IDEV_KBDMOD_IDX_CTRL] = XKB_MOD_NAME_CTRL, + [IDEV_KBDMOD_IDX_ALT] = XKB_MOD_NAME_ALT, + [IDEV_KBDMOD_IDX_LINUX] = XKB_MOD_NAME_LOGO, + [IDEV_KBDMOD_IDX_CAPS] = XKB_MOD_NAME_CAPS, +}; + +static const char * const kbdmap_ledmap[IDEV_KBDLED_CNT] = { + [IDEV_KBDLED_IDX_NUM] = XKB_LED_NAME_NUM, + [IDEV_KBDLED_IDX_CAPS] = XKB_LED_NAME_CAPS, + [IDEV_KBDLED_IDX_SCROLL] = XKB_LED_NAME_SCROLL, +}; + +static kbdmap *kbdmap_ref(kbdmap *km) { + assert_return(km, NULL); + assert_return(km->ref > 0, NULL); + + ++km->ref; + return km; +} + +static kbdmap *kbdmap_unref(kbdmap *km) { + if (!km) + return NULL; + + assert_return(km->ref > 0, NULL); + + if (--km->ref > 0) + return NULL; + + xkb_keymap_unref(km->xkb_keymap); + free(km); + + return 0; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(kbdmap*, kbdmap_unref); + +static int kbdmap_new_from_names(kbdmap **out, + kbdctx *kc, + const char *model, + const char *layout, + const char *variant, + const char *options) { + _cleanup_(kbdmap_unrefp) kbdmap *km = NULL; + struct xkb_rule_names rmlvo = { }; + unsigned int i; + + assert_return(out, -EINVAL); + + km = new0(kbdmap, 1); + if (!km) + return -ENOMEM; + + km->ref = 1; + + rmlvo.rules = "evdev"; + rmlvo.model = model; + rmlvo.layout = layout; + rmlvo.variant = variant; + rmlvo.options = options; + + errno = 0; + km->xkb_keymap = xkb_keymap_new_from_names(kc->xkb_context, &rmlvo, 0); + if (!km->xkb_keymap) + return errno > 0 ? -errno : -EFAULT; + + for (i = 0; i < IDEV_KBDMOD_CNT; ++i) { + const char *t = kbdmap_modmap[i]; + + if (t) + km->modmap[i] = xkb_keymap_mod_get_index(km->xkb_keymap, t); + else + km->modmap[i] = XKB_MOD_INVALID; + } + + for (i = 0; i < IDEV_KBDLED_CNT; ++i) { + const char *t = kbdmap_ledmap[i]; + + if (t) + km->ledmap[i] = xkb_keymap_led_get_index(km->xkb_keymap, t); + else + km->ledmap[i] = XKB_LED_INVALID; + } + + *out = km; + km = NULL; + return 0; +} + +/* + * Keyboard Context + */ + +static void move_str(char **dest, char **src) { + free(*dest); + *dest = *src; + *src = NULL; +} + +static int kbdctx_refresh_keymap(kbdctx *kc) { + idev_session *s; + idev_device *d; + Iterator i, j; + kbdmap *km; + int r; + + if (kc->kbdmap && + streq_ptr(kc->locale_x11_model, kc->last_x11_model) && + streq_ptr(kc->locale_x11_layout, kc->last_x11_layout) && + streq_ptr(kc->locale_x11_variant, kc->last_x11_variant) && + streq_ptr(kc->locale_x11_options, kc->last_x11_options)) + return 0 ; + + move_str(&kc->last_x11_model, &kc->locale_x11_model); + move_str(&kc->last_x11_layout, &kc->locale_x11_layout); + move_str(&kc->last_x11_variant, &kc->locale_x11_variant); + move_str(&kc->last_x11_options, &kc->locale_x11_options); + + log_debug("idev-keyboard: new default keymap: [%s / %s / %s / %s]", + kc->last_x11_model, kc->last_x11_layout, kc->last_x11_variant, kc->last_x11_options); + + /* TODO: add a fallback keymap that's compiled-in */ + r = kbdmap_new_from_names(&km, kc, kc->last_x11_model, kc->last_x11_layout, + kc->last_x11_variant, kc->last_x11_options); + if (r < 0) { + log_debug("idev-keyboard: cannot create keymap from locale1: %s", + strerror(-r)); + return r; + } + + kbdmap_unref(kc->kbdmap); + kc->kbdmap = km; + + HASHMAP_FOREACH(s, kc->context->session_map, i) + HASHMAP_FOREACH(d, s->device_map, j) + if (idev_is_keyboard(d)) + keyboard_update_kbdmap(keyboard_from_device(d)); + + return 0; +} + +static const struct bus_properties_map kbdctx_locale_map[] = { + { "X11Model", "s", NULL, offsetof(kbdctx, locale_x11_model) }, + { "X11Layout", "s", NULL, offsetof(kbdctx, locale_x11_layout) }, + { "X11Variant", "s", NULL, offsetof(kbdctx, locale_x11_variant) }, + { "X11Options", "s", NULL, offsetof(kbdctx, locale_x11_options) }, +}; + +static int kbdctx_locale_get_all_fn(sd_bus *bus, + sd_bus_message *m, + void *userdata, + sd_bus_error *ret_err) { + kbdctx *kc = userdata; + int r; + + kc->slot_locale_get_all = sd_bus_slot_unref(kc->slot_locale_get_all); + + if (sd_bus_message_is_method_error(m, NULL)) { + const sd_bus_error *error = sd_bus_message_get_error(m); + + log_debug("idev-keyboard: GetAll() on locale1 failed: %s: %s", + error->name, error->message); + return 0; + } + + r = bus_message_map_all_properties(bus, m, kbdctx_locale_map, kc); + if (r < 0) { + log_debug("idev-keyboard: erroneous GetAll() reply from locale1"); + return 0; + } + + kbdctx_refresh_keymap(kc); + return 0; +} + +static int kbdctx_query_locale(kbdctx *kc) { + _cleanup_bus_message_unref_ sd_bus_message *m = NULL; + int r; + + kc->slot_locale_get_all = sd_bus_slot_unref(kc->slot_locale_get_all); + + r = sd_bus_message_new_method_call(kc->context->sysbus, + &m, + "org.freedesktop.locale1", + "/org/freedesktop/locale1", + "org.freedesktop.DBus.Properties", + "GetAll"); + if (r < 0) + goto error; + + r = sd_bus_message_append(m, "s", "org.freedesktop.locale1"); + if (r < 0) + goto error; + + r = sd_bus_call_async(kc->context->sysbus, + &kc->slot_locale_get_all, + m, + kbdctx_locale_get_all_fn, + kc, + 0); + if (r < 0) + goto error; + + return 0; + +error: + log_debug("idev-keyboard: cannot send GetAll to locale1: %s", strerror(-r)); + return r; +} + +static int kbdctx_locale_props_changed_fn(sd_bus *bus, + sd_bus_message *signal, + void *userdata, + sd_bus_error *ret_err) { + _cleanup_bus_message_unref_ sd_bus_message *m = NULL; + kbdctx *kc = userdata; + int r; + + kc->slot_locale_get_all = sd_bus_slot_unref(kc->slot_locale_get_all); + + r = bus_message_map_properties_changed(bus, signal, kbdctx_locale_map, kc); + if (r < 0) { + log_debug("idev-keyboard: cannot handle PropertiesChanged from locale1: %s", strerror(-r)); + return r; + } + + if (r > 0) { + r = kbdctx_query_locale(kc); + if (r < 0) + return r; + } + + kbdctx_refresh_keymap(kc); + return 0; +} + +static int kbdctx_setup_bus(kbdctx *kc) { + int r; + + r = sd_bus_add_match(kc->context->sysbus, + &kc->slot_locale_props_changed, + "type='signal'," + "sender='org.freedesktop.locale1'," + "interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged'," + "path='/org/freedesktop/locale1'", + kbdctx_locale_props_changed_fn, + kc); + if (r < 0) { + log_debug("idev-keyboard: cannot setup locale1 link: %s", strerror(-r)); + return r; + } + + return kbdctx_query_locale(kc); +} + +static kbdctx *kbdctx_ref(kbdctx *kc) { + assert_return(kc, NULL); + assert_return(kc->ref > 0, NULL); + + ++kc->ref; + return kc; +} + +static kbdctx *kbdctx_unref(kbdctx *kc) { + if (!kc) + return NULL; + + assert_return(kc->ref > 0, NULL); + + if (--kc->ref > 0) + return NULL; + + free(kc->last_x11_options); + free(kc->last_x11_variant); + free(kc->last_x11_layout); + free(kc->last_x11_model); + free(kc->locale_x11_options); + free(kc->locale_x11_variant); + free(kc->locale_x11_layout); + free(kc->locale_x11_model); + kc->slot_locale_get_all = sd_bus_slot_unref(kc->slot_locale_get_all); + kc->slot_locale_props_changed = sd_bus_slot_unref(kc->slot_locale_props_changed); + kc->kbdmap = kbdmap_unref(kc->kbdmap); + xkb_context_unref(kc->xkb_context); + hashmap_remove_value(kc->context->data_map, KBDCTX_KEY, kc); + free(kc); + + return NULL; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(kbdctx*, kbdctx_unref); + +static int kbdctx_new(kbdctx **out, idev_context *c) { + _cleanup_(kbdctx_unrefp) kbdctx *kc = NULL; + int r; + + assert_return(out, -EINVAL); + assert_return(c, -EINVAL); + + kc = new0(kbdctx, 1); + if (!kc) + return -ENOMEM; + + kc->ref = 1; + kc->context = c; + + errno = 0; + kc->xkb_context = xkb_context_new(0); + if (!kc->xkb_context) + return errno > 0 ? -errno : -EFAULT; + + r = kbdctx_refresh_keymap(kc); + if (r < 0) + return r; + + if (c->sysbus) { + r = kbdctx_setup_bus(kc); + if (r < 0) + return r; + } + + r = hashmap_put(c->data_map, KBDCTX_KEY, kc); + if (r < 0) + return r; + + *out = kc; + kc = NULL; + return 0; +} + +static int get_kbdctx(idev_context *c, kbdctx **out) { + kbdctx *kc; + + assert_return(c, -EINVAL); + assert_return(out, -EINVAL); + + kc = hashmap_get(c->data_map, KBDCTX_KEY); + if (kc) { + *out = kbdctx_ref(kc); + return 0; + } + + return kbdctx_new(out, c); +} + +/* + * Keyboard Devices + */ + +bool idev_is_keyboard(idev_device *d) { + return d && d->vtable == &keyboard_vtable; +} + +idev_device *idev_find_keyboard(idev_session *s, const char *name) { + char *kname; + + assert_return(s, NULL); + assert_return(name, NULL); + + kname = strappenda("keyboard/", name); + return hashmap_get(s->device_map, kname); +} + +static int keyboard_raise_data(idev_keyboard *k, idev_data *data) { + idev_device *d = &k->device; + int r; + + r = idev_session_raise_device_data(d->session, d, data); + if (r < 0) + log_debug("idev-keyboard: %s/%s: error while raising data event: %s", + d->session->name, d->name, strerror(-r)); + + return r; +} + +static void keyboard_arm(idev_keyboard *k, usec_t usecs) { + int r; + + if (usecs != 0) { + usecs += now(CLOCK_MONOTONIC); + r = sd_event_source_set_time(k->repeat_timer, usecs); + if (r >= 0) + sd_event_source_set_enabled(k->repeat_timer, SD_EVENT_ONESHOT); + } else { + sd_event_source_set_enabled(k->repeat_timer, SD_EVENT_OFF); + } +} + +static int keyboard_repeat_timer_fn(sd_event_source *source, uint64_t usec, void *userdata) { + idev_keyboard *k = userdata; + + keyboard_arm(k, k->repeat_rate); + return keyboard_raise_data(k, &k->repdata); +} + +int idev_keyboard_new(idev_device **out, idev_session *s, const char *name) { + _cleanup_(idev_device_freep) idev_device *d = NULL; + idev_keyboard *k; + char *kname; + int r; + + assert_return(out, -EINVAL); + assert_return(s, -EINVAL); + assert_return(name, -EINVAL); + + k = new0(idev_keyboard, 1); + if (!k) + return -ENOMEM; + + d = &k->device; + k->device = IDEV_DEVICE_INIT(&keyboard_vtable, s); + k->repeat_delay = 250 * USEC_PER_MSEC; + k->repeat_rate = 30 * USEC_PER_MSEC; + + /* TODO: add key-repeat configuration */ + + r = get_kbdctx(s->context, &k->kbdctx); + if (r < 0) + return r; + + r = keyboard_update_kbdmap(k); + if (r < 0) + return r; + + r = sd_event_add_time(s->context->event, + &k->repeat_timer, + CLOCK_MONOTONIC, + 0, + 10 * USEC_PER_MSEC, + keyboard_repeat_timer_fn, + k); + if (r < 0) + return r; + + r = sd_event_source_set_enabled(k->repeat_timer, SD_EVENT_OFF); + if (r < 0) + return r; + + kname = strappenda("keyboard/", name); + r = idev_device_add(d, kname); + if (r < 0) + return r; + + if (out) + *out = d; + d = NULL; + return 0; +} + +static void keyboard_free(idev_device *d) { + idev_keyboard *k = keyboard_from_device(d); + + free(k->repdata.keyboard.codepoints); + free(k->repdata.keyboard.keysyms); + free(k->evdata.keyboard.codepoints); + free(k->evdata.keyboard.keysyms); + k->repeat_timer = sd_event_source_unref(k->repeat_timer); + k->kbdmap = kbdmap_unref(k->kbdmap); + k->kbdctx = kbdctx_unref(k->kbdctx); + free(k); +} + +static int8_t guess_ascii(struct xkb_state *state, uint32_t code, uint32_t n_syms, const uint32_t *syms) { + xkb_layout_index_t n_lo, lo; + xkb_level_index_t lv; + struct xkb_keymap *keymap; + const xkb_keysym_t *s; + int num; + + if (n_syms == 1 && syms[0] < 128) + return syms[0]; + + keymap = xkb_state_get_keymap(state); + n_lo = xkb_keymap_num_layouts_for_key(keymap, code + KBDXKB_SHIFT); + + for (lo = 0; lo < n_lo; ++lo) { + lv = xkb_state_key_get_level(state, code + KBDXKB_SHIFT, lo); + num = xkb_keymap_key_get_syms_by_level(keymap, code + KBDXKB_SHIFT, lo, lv, &s); + if (num == 1 && s[0] < 128) + return s[0]; + } + + return -1; +} + +static int keyboard_fill(idev_keyboard *k, + idev_data *dst, + bool resync, + uint16_t code, + uint32_t value, + uint32_t n_syms, + const uint32_t *keysyms) { + idev_data_keyboard *kev; + uint32_t i; + + assert(dst == &k->evdata || dst == &k->repdata); + + if (n_syms > k->n_syms) { + uint32_t *t; + + t = realloc(k->evdata.keyboard.keysyms, sizeof(*t) * n_syms); + if (!t) + return -ENOMEM; + k->evdata.keyboard.keysyms = t; + + t = realloc(k->evdata.keyboard.codepoints, sizeof(*t) * n_syms); + if (!t) + return -ENOMEM; + k->evdata.keyboard.codepoints = t; + + t = realloc(k->repdata.keyboard.keysyms, sizeof(*t) * n_syms); + if (!t) + return -ENOMEM; + k->repdata.keyboard.keysyms = t; + + t = realloc(k->repdata.keyboard.codepoints, sizeof(*t) * n_syms); + if (!t) + return -ENOMEM; + k->repdata.keyboard.codepoints = t; + + k->n_syms = n_syms; + } + + dst->type = IDEV_DATA_KEYBOARD; + dst->resync = resync; + kev = &dst->keyboard; + kev->ascii = guess_ascii(k->xkb_state, code, n_syms, keysyms); + kev->value = value; + kev->keycode = code; + kev->mods = 0; + kev->consumed_mods = 0; + kev->n_syms = n_syms; + memcpy(kev->keysyms, keysyms, sizeof(*keysyms) * n_syms); + + for (i = 0; i < n_syms; ++i) { + kev->codepoints[i] = xkb_keysym_to_utf32(keysyms[i]); + if (!kev->codepoints[i]) + kev->codepoints[i] = 0xffffffffUL; + } + + for (i = 0; i < IDEV_KBDMOD_CNT; ++i) { + int r; + + if (k->kbdmap->modmap[i] == XKB_MOD_INVALID) + continue; + + r = xkb_state_mod_index_is_active(k->xkb_state, k->kbdmap->modmap[i], XKB_STATE_MODS_EFFECTIVE); + if (r > 0) + kev->mods |= 1 << i; + + r = xkb_state_mod_index_is_consumed(k->xkb_state, code + KBDXKB_SHIFT, k->kbdmap->modmap[i]); + if (r > 0) + kev->consumed_mods |= 1 << i; + } + + return 0; +} + +static void keyboard_repeat(idev_keyboard *k) { + idev_data *evdata = &k->evdata; + idev_data *repdata = &k->repdata; + idev_data_keyboard *evkbd = &evdata->keyboard; + idev_data_keyboard *repkbd = &repdata->keyboard; + const xkb_keysym_t *keysyms; + idev_device *d = &k->device; + bool repeats; + int r, num; + + if (evdata->resync) { + /* + * We received a re-sync event. During re-sync, any number of + * key-events may have been lost and sync-events may be + * re-ordered. Always disable key-repeat for those events. Any + * following event will trigger it again. + */ + + k->repeating = false; + keyboard_arm(k, 0); + return; + } + + repeats = xkb_keymap_key_repeats(k->kbdmap->xkb_keymap, evkbd->keycode + KBDXKB_SHIFT); + + if (k->repeating && repkbd->keycode == evkbd->keycode) { + /* + * We received an event for the key we currently repeat. If it + * was released, stop key-repeat. Otherwise, ignore the event. + */ + + if (evkbd->value == KBDKEY_UP) { + k->repeating = false; + keyboard_arm(k, 0); + } + } else if (evkbd->value == KBDKEY_DOWN && repeats) { + /* + * We received a key-down event for a key that repeats. The + * previous condition caught keys we already repeat, so we know + * this is a different key or no key-repeat is running. Start + * new key-repeat. + */ + + errno = 0; + num = xkb_state_key_get_syms(k->xkb_state, evkbd->keycode + KBDXKB_SHIFT, &keysyms); + if (num < 0) + r = errno > 0 ? errno : -EFAULT; + else + r = keyboard_fill(k, repdata, false, evkbd->keycode, KBDKEY_REPEAT, num, keysyms); + + if (r < 0) { + log_debug("idev-keyboard: %s/%s: cannot set key-repeat: %s", + d->session->name, d->name, strerror(-r)); + k->repeating = false; + keyboard_arm(k, 0); + } else { + k->repeating = true; + keyboard_arm(k, k->repeat_delay); + } + } else if (k->repeating && !repeats) { + /* + * We received an event for a key that does not repeat, but we + * currently repeat a previously received key. The new key is + * usually a modifier, but might be any kind of key. In this + * case, we continue repeating the old key, but update the + * symbols according to the new state. + */ + + errno = 0; + num = xkb_state_key_get_syms(k->xkb_state, repkbd->keycode + KBDXKB_SHIFT, &keysyms); + if (num < 0) + r = errno > 0 ? errno : -EFAULT; + else + r = keyboard_fill(k, repdata, false, repkbd->keycode, KBDKEY_REPEAT, num, keysyms); + + if (r < 0) { + log_debug("idev-keyboard: %s/%s: cannot update key-repeat: %s", + d->session->name, d->name, strerror(-r)); + k->repeating = false; + keyboard_arm(k, 0); + } + } +} + +static int keyboard_feed_evdev(idev_keyboard *k, idev_data *data) { + struct input_event *ev = &data->evdev.event; + enum xkb_state_component compch; + const xkb_keysym_t *keysyms; + idev_device *d = &k->device; + int num, r; + + if (ev->type != EV_KEY || ev->value > KBDKEY_DOWN) + return 0; + + /* TODO: We should audit xkb-actions, whether they need @resync as + * flag. Most actions should just be executed, however, there might + * be actions that depend on modifier-orders. Those should be + * suppressed. */ + + num = xkb_state_key_get_syms(k->xkb_state, ev->code + KBDXKB_SHIFT, &keysyms); + compch = xkb_state_update_key(k->xkb_state, ev->code + KBDXKB_SHIFT, ev->value); + + if (compch & XKB_STATE_LEDS) { + /* TODO: update LEDs */ + } + + if (num < 0) + goto error; + + r = keyboard_fill(k, &k->evdata, data->resync, ev->code, ev->value, num, keysyms); + if (r < 0) + goto error; + + keyboard_repeat(k); + return keyboard_raise_data(k, &k->evdata); + +error: + log_debug("idev-keyboard: %s/%s: cannot handle event: %s", + d->session->name, d->name, strerror(-r)); + k->repeating = false; + keyboard_arm(k, 0); + return 0; +} + +static int keyboard_feed(idev_device *d, idev_data *data) { + idev_keyboard *k = keyboard_from_device(d); + + switch (data->type) { + case IDEV_DATA_RESYNC: + /* + * If the underlying device is re-synced, key-events might be + * sent re-ordered. Thus, we don't know which key was pressed + * last. Key-repeat might get confused, hence, disable it + * during re-syncs. The first following event will enable it + * again. + */ + + k->repeating = false; + keyboard_arm(k, 0); + return 0; + case IDEV_DATA_EVDEV: + return keyboard_feed_evdev(k, data); + default: + return 0; + } +} + +static int keyboard_update_kbdmap(idev_keyboard *k) { + idev_device *d = &k->device; + struct xkb_state *state; + kbdmap *km; + int r; + + assert(k); + + km = k->kbdctx->kbdmap; + if (km == k->kbdmap) + return 0; + + errno = 0; + state = xkb_state_new(km->xkb_keymap); + if (!state) { + r = errno > 0 ? -errno : -EFAULT; + goto error; + } + + kbdmap_unref(k->kbdmap); + k->kbdmap = kbdmap_ref(km); + xkb_state_unref(k->xkb_state); + k->xkb_state = state; + + /* TODO: On state-change, we should trigger a resync so the whole + * event-state is flushed into the new xkb-state. libevdev currently + * does not support that, though. */ + + return 0; + +error: + log_debug("idev-keyboard: %s/%s: cannot adopt new keymap: %s", + d->session->name, d->name, strerror(-r)); + return r; +} + +static const idev_device_vtable keyboard_vtable = { + .free = keyboard_free, + .feed = keyboard_feed, +}; diff --git a/src/libsystemd-terminal/idev.c b/src/libsystemd-terminal/idev.c index 2316a66529..0ed518cded 100644 --- a/src/libsystemd-terminal/idev.c +++ b/src/libsystemd-terminal/idev.c @@ -27,6 +27,7 @@ #include #include #include +#include #include "hashmap.h" #include "idev.h" #include "idev-internal.h" @@ -525,10 +526,40 @@ void idev_session_disable(idev_session *s) { } } +static int add_link(idev_element *e, idev_device *d) { + idev_link *l; + + assert(e); + assert(d); + + l = new0(idev_link, 1); + if (!l) + return -ENOMEM; + + l->element = e; + l->device = d; + LIST_PREPEND(links_by_element, e->links, l); + LIST_PREPEND(links_by_device, d->links, l); + device_attach(d, l); + + return 0; +} + +static int guess_type(struct udev_device *d) { + const char *id_key; + + id_key = udev_device_get_property_value(d, "ID_INPUT_KEY"); + if (streq_ptr(id_key, "1")) + return IDEV_DEVICE_KEYBOARD; + + return IDEV_DEVICE_CNT; +} + int idev_session_add_evdev(idev_session *s, struct udev_device *ud) { idev_element *e; + idev_device *d; dev_t devnum; - int r; + int r, type; assert_return(s, -EINVAL); assert_return(ud, -EINVAL); @@ -549,7 +580,34 @@ int idev_session_add_evdev(idev_session *s, struct udev_device *ud) { if (r != 0) return r; - return 0; + type = guess_type(ud); + if (type < 0) + return type; + + switch (type) { + case IDEV_DEVICE_KEYBOARD: + d = idev_find_keyboard(s, e->name); + if (d) { + log_debug("idev: %s: keyboard for new evdev element '%s' already available", + s->name, e->name); + return 0; + } + + r = idev_keyboard_new(&d, s, e->name); + if (r < 0) + return r; + + r = add_link(e, d); + if (r < 0) { + idev_device_free(d); + return r; + } + + return session_add_device(s, d); + default: + /* unknown elements are silently ignored */ + return 0; + } } int idev_session_remove_evdev(idev_session *s, struct udev_device *ud) { diff --git a/src/libsystemd-terminal/idev.h b/src/libsystemd-terminal/idev.h index c98fb1bfb0..0ae044cfd5 100644 --- a/src/libsystemd-terminal/idev.h +++ b/src/libsystemd-terminal/idev.h @@ -32,10 +32,12 @@ #include #include #include +#include #include "util.h" typedef struct idev_data idev_data; typedef struct idev_data_evdev idev_data_evdev; +typedef struct idev_data_keyboard idev_data_keyboard; typedef struct idev_event idev_event; typedef struct idev_device idev_device; @@ -52,6 +54,7 @@ enum { }; enum { + IDEV_DEVICE_KEYBOARD, IDEV_DEVICE_CNT }; @@ -64,12 +67,57 @@ struct idev_data_evdev { }; /* + * Keyboard Devices + */ + +struct xkb_state; + +enum { + IDEV_KBDMOD_IDX_SHIFT, + IDEV_KBDMOD_IDX_CTRL, + IDEV_KBDMOD_IDX_ALT, + IDEV_KBDMOD_IDX_LINUX, + IDEV_KBDMOD_IDX_CAPS, + IDEV_KBDMOD_CNT, + + IDEV_KBDMOD_SHIFT = 1 << IDEV_KBDMOD_IDX_SHIFT, + IDEV_KBDMOD_CTRL = 1 << IDEV_KBDMOD_IDX_CTRL, + IDEV_KBDMOD_ALT = 1 << IDEV_KBDMOD_IDX_ALT, + IDEV_KBDMOD_LINUX = 1 << IDEV_KBDMOD_IDX_LINUX, + IDEV_KBDMOD_CAPS = 1 << IDEV_KBDMOD_IDX_CAPS, +}; + +enum { + IDEV_KBDLED_IDX_NUM, + IDEV_KBDLED_IDX_CAPS, + IDEV_KBDLED_IDX_SCROLL, + IDEV_KBDLED_CNT, + + IDEV_KBDLED_NUM = 1 << IDEV_KBDLED_IDX_NUM, + IDEV_KBDLED_CAPS = 1 << IDEV_KBDLED_IDX_CAPS, + IDEV_KBDLED_SCROLL = 1 << IDEV_KBDLED_IDX_SCROLL, +}; + +struct idev_data_keyboard { + struct xkb_state *xkb_state; + int8_t ascii; + uint8_t value; + uint16_t keycode; + uint32_t mods; + uint32_t consumed_mods; + uint32_t n_syms; + uint32_t *keysyms; + uint32_t *codepoints; +}; + +/* * Data Packets */ enum { IDEV_DATA_RESYNC, IDEV_DATA_EVDEV, + IDEV_DATA_KEYBOARD, IDEV_DATA_CNT }; @@ -79,6 +127,7 @@ struct idev_data { union { idev_data_evdev evdev; + idev_data_keyboard keyboard; }; };