diff options
-rw-r--r-- | Makefile.am | 1 | ||||
-rw-r--r-- | configure.ac | 2 | ||||
-rw-r--r-- | src/libsystemd-terminal/idev-internal.h | 9 | ||||
-rw-r--r-- | src/libsystemd-terminal/idev-keyboard.c | 846 | ||||
-rw-r--r-- | src/libsystemd-terminal/idev.c | 62 | ||||
-rw-r--r-- | src/libsystemd-terminal/idev.h | 49 |
6 files changed, 966 insertions, 3 deletions
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 <stdlib.h> #include <systemd/sd-bus.h> #include <systemd/sd-event.h> +#include <xkbcommon/xkbcommon.h> #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 <dh.herrmann@gmail.com> + + 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 <http://www.gnu.org/licenses/>. +***/ + +#include <inttypes.h> +#include <stdbool.h> +#include <stdlib.h> +#include <systemd/sd-bus.h> +#include <systemd/sd-event.h> +#include <xkbcommon/xkbcommon.h> +#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 <systemd/sd-bus.h> #include <systemd/sd-event.h> #include <systemd/sd-login.h> +#include <xkbcommon/xkbcommon.h> #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 <stdlib.h> #include <systemd/sd-bus.h> #include <systemd/sd-event.h> +#include <xkbcommon/xkbcommon.h> #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; }; }; |