| /*-*- 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 <stdbool.h> |
| #include <stdlib.h> |
| #include <xkbcommon/xkbcommon.h> |
| #include <xkbcommon/xkbcommon-compose.h> |
| #include "sd-bus.h" |
| #include "sd-event.h" |
| #include "hashmap.h" |
| #include "macro.h" |
| #include "util.h" |
| #include "bus-util.h" |
| #include "idev.h" |
| #include "idev-internal.h" |
| #include "term-internal.h" |
| |
| typedef struct kbdtbl kbdtbl; |
| typedef struct kbdmap kbdmap; |
| typedef struct kbdctx kbdctx; |
| typedef struct idev_keyboard idev_keyboard; |
| |
| struct kbdtbl { |
| unsigned long ref; |
| struct xkb_compose_table *xkb_compose_table; |
| }; |
| |
| 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; |
| struct kbdtbl *kbdtbl; |
| |
| sd_bus_slot *slot_locale_props_changed; |
| sd_bus_slot *slot_locale_get_all; |
| |
| char *locale_lang; |
| 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; |
| kbdtbl *kbdtbl; |
| |
| struct xkb_state *xkb_state; |
| struct xkb_compose_state *xkb_compose; |
| |
| usec_t repeat_delay; |
| usec_t repeat_rate; |
| sd_event_source *repeat_timer; |
| |
| uint32_t n_syms; |
| idev_data evdata; |
| idev_data repdata; |
| uint32_t *compose_res; |
| |
| 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); |
| static int keyboard_update_kbdtbl(idev_keyboard *k); |
| |
| /* |
| * Keyboard Compose Tables |
| */ |
| |
| static kbdtbl *kbdtbl_ref(kbdtbl *kt) { |
| if (kt) { |
| assert_return(kt->ref > 0, NULL); |
| ++kt->ref; |
| } |
| |
| return kt; |
| } |
| |
| static kbdtbl *kbdtbl_unref(kbdtbl *kt) { |
| if (!kt) |
| return NULL; |
| |
| assert_return(kt->ref > 0, NULL); |
| |
| if (--kt->ref > 0) |
| return NULL; |
| |
| xkb_compose_table_unref(kt->xkb_compose_table); |
| free(kt); |
| |
| return 0; |
| } |
| |
| DEFINE_TRIVIAL_CLEANUP_FUNC(kbdtbl*, kbdtbl_unref); |
| |
| static int kbdtbl_new_from_locale(kbdtbl **out, kbdctx *kc, const char *locale) { |
| _cleanup_(kbdtbl_unrefp) kbdtbl *kt = NULL; |
| |
| assert_return(out, -EINVAL); |
| assert_return(locale, -EINVAL); |
| |
| kt = new0(kbdtbl, 1); |
| if (!kt) |
| return -ENOMEM; |
| |
| kt->ref = 1; |
| |
| kt->xkb_compose_table = xkb_compose_table_new_from_locale(kc->xkb_context, |
| locale, |
| XKB_COMPOSE_COMPILE_NO_FLAGS); |
| if (!kt->xkb_compose_table) |
| return errno > 0 ? -errno : -EFAULT; |
| |
| *out = kt; |
| kt = NULL; |
| return 0; |
| } |
| |
| /* |
| * 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 int kbdctx_refresh_compose_table(kbdctx *kc, const char *lang) { |
| kbdtbl *kt; |
| idev_session *s; |
| idev_device *d; |
| Iterator i, j; |
| int r; |
| |
| if (!lang) |
| lang = "C"; |
| |
| if (streq_ptr(kc->locale_lang, lang)) |
| return 0; |
| |
| r = free_and_strdup(&kc->locale_lang, lang); |
| if (r < 0) |
| return r; |
| |
| log_debug("idev-keyboard: new default compose table: [ %s ]", lang); |
| |
| r = kbdtbl_new_from_locale(&kt, kc, lang); |
| if (r < 0) { |
| /* TODO: We need to catch the case where no compose-file is |
| * available. xkb doesn't tell us so far.. so we must not treat |
| * it as a hard-failure but just continue. Preferably, we want |
| * xkb to tell us exactly whether compilation failed or whether |
| * there is no compose file available for this locale. */ |
| log_debug_errno(r, "idev-keyboard: cannot load compose-table for '%s': %m", |
| lang); |
| r = 0; |
| kt = NULL; |
| } |
| |
| kbdtbl_unref(kc->kbdtbl); |
| kc->kbdtbl = kt; |
| |
| HASHMAP_FOREACH(s, kc->context->session_map, i) |
| HASHMAP_FOREACH(d, s->device_map, j) |
| if (idev_is_keyboard(d)) |
| keyboard_update_kbdtbl(keyboard_from_device(d)); |
| |
| return 0; |
| } |
| |
| 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) |
| return log_debug_errno(r, "idev-keyboard: cannot create keymap from locale1: %m"); |
| |
| 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 int kbdctx_set_locale(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { |
| kbdctx *kc = userdata; |
| const char *s, *ctype = NULL, *lang = NULL; |
| int r; |
| |
| r = sd_bus_message_enter_container(m, 'a', "s"); |
| if (r < 0) |
| goto error; |
| |
| while ((r = sd_bus_message_read(m, "s", &s)) > 0) { |
| if (!ctype) |
| ctype = startswith(s, "LC_CTYPE="); |
| if (!lang) |
| lang = startswith(s, "LANG="); |
| } |
| |
| if (r < 0) |
| goto error; |
| |
| r = sd_bus_message_exit_container(m); |
| if (r < 0) |
| goto error; |
| |
| kbdctx_refresh_compose_table(kc, ctype ? : lang); |
| r = 0; |
| |
| error: |
| if (r < 0) |
| log_debug_errno(r, "idev-keyboard: cannot parse locale property from locale1: %m"); |
| |
| return r; |
| } |
| |
| static const struct bus_properties_map kbdctx_locale_map[] = { |
| { "Locale", "as", kbdctx_set_locale, 0 }, |
| { "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_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(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: |
| return log_debug_errno(r, "idev-keyboard: cannot send GetAll to locale1: %m"); |
| } |
| |
| static int kbdctx_locale_props_changed_fn(sd_bus_message *signal, |
| 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); |
| |
| /* skip interface name */ |
| r = sd_bus_message_skip(signal, "s"); |
| if (r < 0) |
| goto error; |
| |
| r = bus_message_map_properties_changed(signal, kbdctx_locale_map, kc); |
| if (r < 0) |
| goto error; |
| |
| if (r > 0) { |
| r = kbdctx_query_locale(kc); |
| if (r < 0) |
| return r; |
| } |
| |
| kbdctx_refresh_keymap(kc); |
| return 0; |
| |
| error: |
| return log_debug_errno(r, "idev-keyboard: cannot handle PropertiesChanged from locale1: %m"); |
| } |
| |
| 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) |
| return log_debug_errno(r, "idev-keyboard: cannot setup locale1 link: %m"); |
| |
| return kbdctx_query_locale(kc); |
| } |
| |
| static void kbdctx_log_fn(struct xkb_context *ctx, enum xkb_log_level lvl, const char *format, va_list args) { |
| char buf[LINE_MAX]; |
| int sd_lvl; |
| |
| if (lvl >= XKB_LOG_LEVEL_DEBUG) |
| sd_lvl = LOG_DEBUG; |
| else if (lvl >= XKB_LOG_LEVEL_INFO) |
| sd_lvl = LOG_INFO; |
| else if (lvl >= XKB_LOG_LEVEL_WARNING) |
| sd_lvl = LOG_INFO; /* most XKB warnings really are informational */ |
| else |
| /* XKB_LOG_LEVEL_ERROR and worse */ |
| sd_lvl = LOG_ERR; |
| |
| snprintf(buf, sizeof(buf), "idev-xkb: %s", format); |
| log_internalv(sd_lvl, 0, __FILE__, __LINE__, __func__, buf, args); |
| } |
| |
| 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); |
| free(kc->locale_lang); |
| 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->kbdtbl = kbdtbl_unref(kc->kbdtbl); |
| 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(XKB_CONTEXT_NO_FLAGS); |
| if (!kc->xkb_context) |
| return errno > 0 ? -errno : -EFAULT; |
| |
| xkb_context_set_log_fn(kc->xkb_context, kbdctx_log_fn); |
| xkb_context_set_log_level(kc->xkb_context, XKB_LOG_LEVEL_DEBUG); |
| |
| r = kbdctx_refresh_keymap(kc); |
| if (r < 0) |
| return r; |
| |
| r = kbdctx_refresh_compose_table(kc, NULL); |
| 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 = strjoina("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_errno(r, "idev-keyboard: %s/%s: error while raising data event: %m", |
| d->session->name, d->name); |
| |
| return r; |
| } |
| |
| static int keyboard_resize_bufs(idev_keyboard *k, uint32_t n_syms) { |
| uint32_t *t; |
| |
| if (n_syms <= k->n_syms) |
| return 0; |
| |
| t = realloc(k->compose_res, sizeof(*t) * n_syms); |
| if (!t) |
| return -ENOMEM; |
| k->compose_res = 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; |
| return 0; |
| } |
| |
| static unsigned int keyboard_read_compose(idev_keyboard *k, const xkb_keysym_t **out) { |
| _cleanup_free_ char *t = NULL; |
| term_utf8 u8 = { }; |
| char buf[256], *p; |
| size_t flen = 0; |
| int i, r; |
| |
| r = xkb_compose_state_get_utf8(k->xkb_compose, buf, sizeof(buf)); |
| if (r >= (int)sizeof(buf)) { |
| t = malloc(r + 1); |
| if (!t) |
| return 0; |
| |
| xkb_compose_state_get_utf8(k->xkb_compose, t, r + 1); |
| p = t; |
| } else { |
| p = buf; |
| } |
| |
| for (i = 0; i < r; ++i) { |
| uint32_t *ucs; |
| size_t len, j; |
| |
| len = term_utf8_decode(&u8, &ucs, p[i]); |
| if (len > 0) { |
| r = keyboard_resize_bufs(k, flen + len); |
| if (r < 0) |
| return 0; |
| |
| for (j = 0; j < len; ++j) |
| k->compose_res[flen++] = ucs[j]; |
| } |
| } |
| |
| *out = k->compose_res; |
| return flen; |
| } |
| |
| 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; |
| |
| /* never feed REPEAT keys into COMPOSE */ |
| |
| 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 = keyboard_update_kbdtbl(k); |
| if (r < 0) |
| return r; |
| |
| r = keyboard_resize_bufs(k, 8); |
| 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 = strjoina("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); |
| |
| xkb_compose_state_unref(k->xkb_compose); |
| xkb_state_unref(k->xkb_state); |
| free(k->repdata.keyboard.codepoints); |
| free(k->repdata.keyboard.keysyms); |
| free(k->evdata.keyboard.codepoints); |
| free(k->evdata.keyboard.keysyms); |
| free(k->compose_res); |
| k->repeat_timer = sd_event_source_unref(k->repeat_timer); |
| k->kbdtbl = kbdtbl_unref(k->kbdtbl); |
| 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 && syms[0] > 0) |
| 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 && s[0] > 0) |
| 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; |
| int r; |
| |
| assert(dst == &k->evdata || dst == &k->repdata); |
| |
| r = keyboard_resize_bufs(k, n_syms); |
| if (r < 0) |
| return r; |
| |
| 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) { |
| 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_errno(r, "idev-keyboard: %s/%s: cannot set key-repeat: %m", |
| d->session->name, d->name); |
| 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_errno(r, "idev-keyboard: %s/%s: cannot update key-repeat: %m", |
| d->session->name, d->name); |
| 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; |
| enum xkb_compose_status cstatus; |
| 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) { |
| r = num; |
| goto error; |
| } |
| |
| if (k->xkb_compose && ev->value == KBDKEY_DOWN) { |
| if (num == 1 && !data->resync) { |
| xkb_compose_state_feed(k->xkb_compose, keysyms[0]); |
| cstatus = xkb_compose_state_get_status(k->xkb_compose); |
| } else { |
| cstatus = XKB_COMPOSE_CANCELLED; |
| } |
| |
| switch (cstatus) { |
| case XKB_COMPOSE_NOTHING: |
| /* keep produced keysyms and forward unchanged */ |
| break; |
| case XKB_COMPOSE_COMPOSING: |
| /* consumed by compose-state, drop keysym */ |
| keysyms = NULL; |
| num = 0; |
| break; |
| case XKB_COMPOSE_COMPOSED: |
| /* compose-state produced sth, replace keysym */ |
| num = keyboard_read_compose(k, &keysyms); |
| xkb_compose_state_reset(k->xkb_compose); |
| break; |
| case XKB_COMPOSE_CANCELLED: |
| /* canceled compose, reset, forward cancellation sym */ |
| xkb_compose_state_reset(k->xkb_compose); |
| break; |
| } |
| } else if (k->xkb_compose && |
| num == 1 && |
| keysyms[0] == XKB_KEY_Multi_key && |
| !data->resync && |
| ev->value == KBDKEY_UP) { |
| /* Reset compose state on Multi-Key UP events. This effectively |
| * requires you to hold the key during the whole sequence. I |
| * think it's pretty handy to avoid accidental |
| * Compose-sequences, but this may break Compose for disabled |
| * people. We really need to make this opional! (TODO) */ |
| xkb_compose_state_reset(k->xkb_compose); |
| } |
| |
| if (ev->value == KBDKEY_UP) { |
| /* never produce keysyms for UP */ |
| keysyms = NULL; |
| num = 0; |
| } |
| |
| 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_errno(r, "idev-keyboard: %s/%s: cannot handle event: %m", |
| d->session->name, d->name); |
| 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: |
| return log_debug_errno(r, "idev-keyboard: %s/%s: cannot adopt new keymap: %m", |
| d->session->name, d->name); |
| } |
| |
| static int keyboard_update_kbdtbl(idev_keyboard *k) { |
| idev_device *d = &k->device; |
| struct xkb_compose_state *compose = NULL; |
| kbdtbl *kt; |
| int r; |
| |
| assert(k); |
| |
| kt = k->kbdctx->kbdtbl; |
| if (kt == k->kbdtbl) |
| return 0; |
| |
| if (kt) { |
| errno = 0; |
| compose = xkb_compose_state_new(kt->xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); |
| if (!compose) { |
| r = errno > 0 ? -errno : -EFAULT; |
| goto error; |
| } |
| } |
| |
| kbdtbl_unref(k->kbdtbl); |
| k->kbdtbl = kbdtbl_ref(kt); |
| xkb_compose_state_unref(k->xkb_compose); |
| k->xkb_compose = compose; |
| |
| return 0; |
| |
| error: |
| return log_debug_errno(r, "idev-keyboard: %s/%s: cannot adopt new compose table: %m", |
| d->session->name, d->name); |
| } |
| |
| static const idev_device_vtable keyboard_vtable = { |
| .free = keyboard_free, |
| .feed = keyboard_feed, |
| }; |