From ef594b877ba8e2e820cd1f9d9e1dca3e1de21c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Dec 2018 19:59:29 +0100 Subject: [PATCH] module/xkb: monitor current xkb layout --- CMakeLists.txt | 5 + config.c | 13 ++ modules/xkb/xkb.c | 474 ++++++++++++++++++++++++++++++++++++++++++++++ modules/xkb/xkb.h | 5 + 4 files changed, 497 insertions(+) create mode 100644 modules/xkb/xkb.c create mode 100644 modules/xkb/xkb.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f34aa44..3d454ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ pkg_check_modules(XCB REQUIRED xcb xcb-randr xcb-render) # Core pkg_check_modules(CAIRO REQUIRED cairo cairo-xcb) # Core pkg_check_modules(YAML REQUIRED yaml-0.1) # Core (configuration) +pkg_check_modules(XCB_XKB REQUIRED xcb-xkb) # Module/xkb pkg_check_modules(JSON REQUIRED json-c) # Module/i3 pkg_check_modules(UDEV REQUIRED libudev) # Module/battery @@ -38,6 +39,7 @@ add_executable(f00bar modules/i3/dynlist-exposable.c modules/i3/dynlist-exposable.h modules/i3/i3.c modules/i3/i3.h modules/label/label.c modules/label/label.h + modules/xkb/xkb.c modules/xkb/xkb.h modules/xwindow/xwindow.c modules/xwindow/xwindow.h ) @@ -47,6 +49,7 @@ target_compile_options(f00bar PRIVATE ${XCB_CFLAGS_OTHER} ${CAIRO_CFLAGS_OTHER} ${YAML_CFLAGS_OTHER} + ${XCB_XKB_CFLAGS_OTHER} ${JSON_CFLAGS_OTHER} ${UDEV_CFLAGS_OTHER} ) @@ -55,6 +58,7 @@ target_include_directories(f00bar PRIVATE ${XCB_INCLUDE_DIRS} ${CAIRO_INCLUDE_DIRS} ${YAML_INCLUDE_DIRS} + ${XCB_XKB_INCLUDE_DIRS} ${JSON_INCLUDE_DIRS} ${UDEV_INCLUDE_DIRS} ) @@ -64,6 +68,7 @@ target_link_libraries(f00bar ${XCB_LIBRARIES} ${CAIRO_LIBRARIES} ${YAML_LIBRARIES} + ${XCB_XKB_LIBRARIES} ${JSON_LIBRARIES} ${UDEV_LIBRARIES} ) diff --git a/config.c b/config.c index 3efa18c..82e5037 100644 --- a/config.c +++ b/config.c @@ -18,6 +18,7 @@ #include "modules/clock/clock.h" #include "modules/i3/i3.h" #include "modules/label/label.h" +#include "modules/xkb/xkb.h" #include "modules/xwindow/xwindow.h" static uint8_t @@ -315,6 +316,16 @@ module_battery_from_config(const struct yml_node *node, poll_interval != NULL ? yml_value_as_int(poll_interval) : 30); } +static struct module * +module_xkb_from_config(const struct yml_node *node, + const struct font *parent_font) +{ + const struct yml_node *c = yml_get_value(node, "content"); + assert(yml_is_dict(c)); + + return module_xkb(particle_from_config(c, parent_font)); +} + struct bar * conf_to_bar(const struct yml_node *bar) { @@ -419,6 +430,8 @@ conf_to_bar(const struct yml_node *bar) mods[idx] = module_i3_from_config(it.node, font); else if (strcmp(mod_name, "battery") == 0) mods[idx] = module_battery_from_config(it.node, font); + else if (strcmp(mod_name, "xkb") == 0) + mods[idx] = module_xkb_from_config(it.node, font); else assert(false); } diff --git a/modules/xkb/xkb.c b/modules/xkb/xkb.c new file mode 100644 index 0000000..58e4e5b --- /dev/null +++ b/modules/xkb/xkb.c @@ -0,0 +1,474 @@ +#include "xkb.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "../../bar.h" +#include "../../xcb.h" + +struct layout { + char *name; + char *symbol; +}; + +struct layouts { + ssize_t count; + struct layout *layouts; +}; + +struct private { + struct particle *label; + struct layouts layouts; + size_t current; +}; + +static void +free_layouts(struct layouts layouts) +{ + for (ssize_t i = 0; i < layouts.count; i++) { + free(layouts.layouts[i].name); + free(layouts.layouts[i].symbol); + } + free(layouts.layouts); +} + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + m->label->destroy(m->label); + free_layouts(m->layouts); + free(m); + free(mod); +} + +static struct exposable * +content(const struct module *mod) +{ + const struct private *m = mod->private; + + printf("current: %zu\n", m->current); + for (size_t i = 0; i < m->layouts.count; i++) + printf(" %s (%s)\n", m->layouts.layouts[i].name, m->layouts.layouts[i].symbol); + + struct tag_set tags = { + .tags = (struct tag *[]){ + tag_new_string("name", m->layouts.layouts[m->current].name), + tag_new_string("symbol", m->layouts.layouts[m->current].symbol)}, + .count = 2, + }; + + struct exposable *exposable = m->label->instantiate(m->label, &tags); + + tag_set_destroy(&tags); + return exposable; +} + +static bool +xkb_enable(xcb_connection_t *conn) +{ + xcb_generic_error_t *err; + + xcb_xkb_use_extension_cookie_t cookie = xcb_xkb_use_extension( + conn, XCB_XKB_MAJOR_VERSION, XCB_XKB_MINOR_VERSION); + xcb_xkb_use_extension_reply_t *reply = xcb_xkb_use_extension_reply( + conn, cookie, &err); + + assert(err == NULL && "xcb_xkb_use_extension() failed"); + assert(reply->supported && "XKB extension not supported"); + free(reply); + return true; +} + +static int +get_xkb_event_base(xcb_connection_t *conn) +{ + const struct xcb_query_extension_reply_t *reply = xcb_get_extension_data( + conn, &xcb_xkb_id); + + if (reply == NULL) { + puts("failed to get XKB extension data"); + return -1; + } + + if (!reply->present) { + puts("XKB not present"); + return -1; + } + + return reply->first_event; +} + +static struct layouts +get_layouts(xcb_connection_t *conn) +{ + struct layouts ret; + xcb_generic_error_t *err; + + xcb_xkb_get_names_cookie_t cookie = xcb_xkb_get_names( + conn, + XCB_XKB_ID_USE_CORE_KBD, + XCB_XKB_NAME_DETAIL_GROUP_NAMES | + XCB_XKB_NAME_DETAIL_SYMBOLS); + + xcb_xkb_get_names_reply_t *reply = xcb_xkb_get_names_reply( + conn, cookie, &err); + + if (err != NULL) { + printf("%d: %s\n", err->error_code, "failed to get group names and symbols"); + free(err); + return (struct layouts){.count = -1}; + } + + xcb_xkb_get_names_value_list_t vlist; + void *buf = xcb_xkb_get_names_value_list(reply); + xcb_xkb_get_names_value_list_unpack( + buf, reply->nTypes, reply->indicators, reply->virtualMods, + reply->groupNames, reply->nKeys, reply->nKeyAliases, + reply->nRadioGroups, reply->which, &vlist); + + /* Number of groups (aka layouts) */ + ret.count = xcb_xkb_get_names_value_list_groups_length(reply, &vlist); + ret.layouts = calloc(ret.count, sizeof(ret.layouts[0])); + + xcb_get_atom_name_cookie_t symbols_name_cookie = xcb_get_atom_name( + conn, vlist.symbolsName); + + xcb_get_atom_name_cookie_t group_name_cookies[ret.count]; + for (ssize_t i = 0; i < ret.count; i++) + group_name_cookies[i] = xcb_get_atom_name(conn, vlist.groups[i]); + + /* Get layout short names (e.g. "us") */ + xcb_get_atom_name_reply_t *atom_name = xcb_get_atom_name_reply( + conn, symbols_name_cookie, &err); + if (err != NULL) { + printf("%d: failed to get atom name\n", err->error_code); + free(err); + goto err; + } + + char *symbols = strndup(xcb_get_atom_name_name(atom_name), + xcb_get_atom_name_name_length(atom_name)); + free(atom_name); + + /* Get layout long names (e.g. "English (US)") */ + for (ssize_t i = 0; i < ret.count; i++) { + atom_name = xcb_get_atom_name_reply(conn, group_name_cookies[i], &err); + if (err != NULL) { + printf("%d: failed to get atom name\n", err->error_code); + free(err); + goto err; + } + + ret.layouts[i].name = strndup(xcb_get_atom_name_name(atom_name), + xcb_get_atom_name_name_length(atom_name)); + free(atom_name); + } + + /* e.g. pc+us+inet(evdev)+group(..) */ + ssize_t layout_idx = 0; + for (char *tok_ctx = NULL, *tok = strtok_r(symbols, "+", &tok_ctx); + tok != NULL; + tok = strtok_r(NULL, "+", &tok_ctx)) { + + char *fname = strtok(tok, "()"); + char *section __attribute__((unused)) = strtok(NULL, "()"); + + /* Not sure why some layouts have a ":n" suffix (where + * 'n' is a number, e.g. "us:2") */ + fname = strtok(fname, ":"); + + /* Assume all language layouts are two-letters */ + if (strlen(fname) != 2) + continue; + + /* But make sure to ignore "pc" :) */ + if (strcmp(tok, "pc") == 0) + continue; + + if (layout_idx >= ret.count) { + printf("layout vs group name count mismatch: %zd > %zd\n", + layout_idx + 1, ret.count); + goto err; + } + + char *sym = strdup(fname); + if (sym == NULL) { + printf("failed to allocate memory for symbol: %s (%d)", + strerror(errno), errno); + goto err; + } + + ret.layouts[layout_idx++].symbol = sym; + } + + if (layout_idx != ret.count) { + printf("layout vs group name count mismatch: %zd != %zd\n", + layout_idx, ret.count); + goto err; + } + + free(symbols); + free(reply); + + return ret; + +err: + free(symbols); + free(reply); + free_layouts(ret); + return (struct layouts){.count = -1}; +} + +static int +get_current_layout(xcb_connection_t *conn) +{ + xcb_generic_error_t *err; + + xcb_xkb_get_state_cookie_t cookie = xcb_xkb_get_state( + conn, XCB_XKB_ID_USE_CORE_KBD); + xcb_xkb_get_state_reply_t *reply = xcb_xkb_get_state_reply( + conn, cookie, &err); + + if (err != NULL) { + printf("%d: %s\n", err->error_code, "failed to get XKB state"); + free(err); + return -1; + } + + int ret = reply->group; + + free(reply); + return ret; +} + +static bool +register_for_events(xcb_connection_t *conn) +{ + xcb_void_cookie_t cookie = xcb_xkb_select_events_checked( + conn, + XCB_XKB_ID_USE_CORE_KBD, + ( + XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY | + XCB_XKB_EVENT_TYPE_STATE_NOTIFY | + XCB_XKB_EVENT_TYPE_MAP_NOTIFY | + XCB_XKB_EVENT_TYPE_INDICATOR_STATE_NOTIFY + ), + 0, + ( + XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY | + XCB_XKB_EVENT_TYPE_STATE_NOTIFY | + XCB_XKB_EVENT_TYPE_MAP_NOTIFY | + XCB_XKB_EVENT_TYPE_INDICATOR_STATE_NOTIFY + ), + 0, 0, NULL); + + xcb_generic_error_t *err = xcb_request_check(conn, cookie); + if (err != NULL) { + printf("%d: %s\n", err->error_code, "failed to register for events"); + free(err); + return false; + } + + return true; +} + +static bool +event_loop(int abort_fd, const struct bar *bar, struct private *m, + xcb_connection_t *conn, int xkb_event_base) +{ + bool ret = false; + bool has_error = false; + + const int xcb_fd = xcb_get_file_descriptor(conn); + assert(xcb_fd >= 0); + + while (!has_error) { + struct pollfd pfds[] = { + {.fd = abort_fd, .events = POLLIN }, + {.fd = xcb_fd, .events = POLLIN | POLLHUP } + }; + + /* Use poll() since xcb_wait_for_events() doesn't return on signals */ + int pret = poll(pfds, sizeof(pfds) / sizeof(pfds[0]), -1); + if (pfds[0].revents & POLLIN) { + ret = true; + break; + } + + assert(pret == 1 && "unexpected poll(3) return value"); + + if (pfds[1].revents & POLLHUP) { + puts("I/O error, server disconnect?"); + break; + } + + assert(pfds[1].revents & POLLIN && "POLLIN not set"); + + /* + * Note: poll() might have returned *before* the entire event + * has been received, and thus we might block here. Hopefully + * not for long though... + */ + + for (xcb_generic_event_t *_evt = xcb_wait_for_event(conn); + _evt != NULL; + _evt = xcb_poll_for_event(conn)) { + + if (_evt->response_type != xkb_event_base) { + printf("non-XKB event ignored: %d\n", _evt->response_type); + free(_evt); + continue; + } + + switch(_evt->pad0) { + default: + printf("unimplemented XKB event: %d\n", _evt->pad0); + break; + + case XCB_XKB_NEW_KEYBOARD_NOTIFY: { + int current = get_current_layout(conn); + if (current == -1) { + has_error = true; + break; + } + + struct layouts layouts = get_layouts(conn); + if (layouts.count == -1) { + has_error = true; + break; + } + + if (current < layouts.count) { + free_layouts(m->layouts); + m->layouts = layouts;//json_all(layouts, current); + m->current = current; + bar->refresh(bar); + } else { + /* Can happen while transitioning to a new map */ + free_layouts(layouts); + } + + //free_layouts(layouts); + break; + } + + case XCB_XKB_STATE_NOTIFY: { + const xcb_xkb_state_notify_event_t *evt = + (const xcb_xkb_state_notify_event_t *)_evt; + + if (evt->changed & XCB_XKB_STATE_PART_GROUP_STATE) { + ;//json_layout_change(evt->group); + m->current = evt->group; + bar->refresh(bar); + } + + break; + } + + case XCB_XKB_MAP_NOTIFY: + printf("map event unimplemented\n"); + break; + + case XCB_XKB_INDICATOR_STATE_NOTIFY: + printf("indicator state event unimplemented\n"); + break; + } + + free(_evt); + } + } + + return ret; +} + +static bool +talk_to_xkb(int abort_fd, const struct bar *bar, struct private *m, + xcb_connection_t *conn) +{ + if (!xkb_enable(conn)) + return false; + + if (!register_for_events(conn)) + return false; + + int xkb_event_base = get_xkb_event_base(conn); + if (xkb_event_base == -1) + return false; + + int current = get_current_layout(conn); + if (current == -1) + return false; + + struct layouts layouts = get_layouts(conn); + if (layouts.count == -1) + return false; + + if (current >= layouts.count) { + printf("current layout index: %d >= %zd\n", current, layouts.count); + free_layouts(layouts); + return false; + } + + //json_all(layouts, current); + m->layouts = layouts; + m->current = current; + bar->refresh(bar); + //free_layouts(layouts); + +#if 0 + /* Signal handlers (disconnects from X) */ + const struct sigaction sa = { .sa_handler = &sighandler }; + if (sigaction(SIGINT, &sa, NULL) == -1 || + sigaction(SIGTERM, &sa, NULL) == -1) { + + json_errno("failed to register signal handlers"); + return false; + } +#endif + + return event_loop(abort_fd, bar, m, conn, xkb_event_base); +} + +static int +run(struct module_run_context *ctx) +{ + //struct private *m = ctx->module->private; + xcb_connection_t *conn = xcb_connect(NULL, NULL); + if (conn == NULL) { + puts("failed to connect to X server"); + return EXIT_FAILURE; + } + + int ret = talk_to_xkb(ctx->abort_fd, ctx->module->bar, ctx->module->private, conn) + ? EXIT_SUCCESS : EXIT_FAILURE; + + xcb_disconnect(conn); + return ret; +} + +struct module * +module_xkb(struct particle *label) +{ + struct private *m = malloc(sizeof(*m)); + m->label = label; + + struct module *mod = malloc(sizeof(*mod)); + mod->bar = NULL; + mod->private = m; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + mod->begin_expose = &module_default_begin_expose; + mod->expose = &module_default_expose; + mod->end_expose = &module_default_end_expose; + return mod; +} diff --git a/modules/xkb/xkb.h b/modules/xkb/xkb.h new file mode 100644 index 0000000..c23129b --- /dev/null +++ b/modules/xkb/xkb.h @@ -0,0 +1,5 @@ +#pragma once +#include "../../module.h" +#include "../../particle.h" + +struct module *module_xkb(struct particle *label);