#include #include #include #include #include #include #include #define LOG_MODULE "xkb" #define LOG_ENABLE_DBG 0 #include "../log.h" #include "../bar/bar.h" #include "../config.h" #include "../config-verify.h" #include "../plugin.h" #include "../xcb.h" struct layout { char *name; char *symbol; }; struct layouts { size_t count; struct layout *layouts; }; struct indicators { size_t count; char **names; }; struct private { struct particle *label; struct indicators indicators; struct layouts layouts; size_t current; bool caps_lock; bool num_lock; bool scroll_lock; }; static void free_layouts(struct layouts layouts) { for (size_t i = 0; i < layouts.count; i++) { free(layouts.layouts[i].name); free(layouts.layouts[i].symbol); } free(layouts.layouts); } static void free_indicators(struct indicators indicators) { for (size_t i = 0; i < indicators.count; i++) free(indicators.names[i]); free(indicators.names); } static void destroy(struct module *mod) { struct private *m = mod->private; m->label->destroy(m->label); free_layouts(m->layouts); free_indicators(m->indicators); free(m); module_default_destroy(mod); } static const char * description(struct module *mod) { return "xkb"; } static struct exposable * content(struct module *mod) { const struct private *m = mod->private; mtx_lock(&mod->lock); const char *name = ""; const char *symbol = ""; if (m->current < m->layouts.count) { name = m->layouts.layouts[m->current].name; symbol = m->layouts.layouts[m->current].symbol; } struct tag_set tags = { .tags = (struct tag *[]){ tag_new_string(mod, "name", name), tag_new_string(mod, "symbol", symbol), tag_new_bool(mod, "caps_lock", m->caps_lock), tag_new_bool(mod, "num_lock", m->num_lock), tag_new_bool(mod, "scroll_lock", m->scroll_lock), }, .count = 5, }; mtx_unlock(&mod->lock); 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); if (err != NULL) { LOG_ERR("failed to query for XKB extension: %s", xcb_error(err)); free(err); free(reply); return false; } if (!reply->supported) { LOG_ERR("XKB extension is not supported"); free(reply); return false; } 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) { LOG_ERR("failed to get XKB extension data"); return -1; } if (!reply->present) { LOG_ERR("XKB not present"); return -1; } return reply->first_event; } static bool get_layouts_and_indicators(xcb_connection_t *conn, struct layouts *layouts, struct indicators *indicators) { 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_NAME_DETAIL_INDICATOR_NAMES); xcb_xkb_get_names_reply_t *reply = xcb_xkb_get_names_reply( conn, cookie, &err); if (err != NULL) { LOG_ERR("failed to get layouts and indicators: %s", xcb_error(err)); free(err); return false; } 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) */ layouts->count = xcb_xkb_get_names_value_list_groups_length(reply, &vlist); layouts->layouts = calloc(layouts->count, sizeof(layouts->layouts[0])); /* Number of indicators */ indicators->count = xcb_xkb_get_names_value_list_indicator_names_length( reply, &vlist); indicators->names = calloc(indicators->count, sizeof(indicators->names[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[layouts->count]; for (size_t i = 0; i < layouts->count; i++) group_name_cookies[i] = xcb_get_atom_name(conn, vlist.groups[i]); xcb_get_atom_name_cookie_t indicator_cookies[indicators->count]; for (size_t i = 0; i < indicators->count; i++) indicator_cookies[i] = xcb_get_atom_name(conn, vlist.indicatorNames[i]); char *symbols = NULL; /* 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) { LOG_ERR("failed to get 'symbols' atom name: %s", xcb_error(err)); free(err); goto err; } symbols = strndup( xcb_get_atom_name_name(atom_name), xcb_get_atom_name_name_length(atom_name)); LOG_DBG("symbols: %s", symbols); free(atom_name); /* Get layout long names (e.g. "English (US)") */ for (size_t i = 0; i < layouts->count; i++) { atom_name = xcb_get_atom_name_reply(conn, group_name_cookies[i], &err); if (err != NULL) { LOG_ERR("failed to get 'group' atom name: %s", xcb_error(err)); free(err); goto err; } layouts->layouts[i].name = strndup( xcb_get_atom_name_name(atom_name), xcb_get_atom_name_name_length(atom_name)); LOG_DBG("layout #%zd: long name: %s", i, layouts->layouts[i].name); free(atom_name); } /* Indicator names e.g. "Caps Lock", "Num Lock" */ for (size_t i = 0; i < indicators->count; i++) { atom_name = xcb_get_atom_name_reply(conn, indicator_cookies[i], &err); if (err != NULL) { LOG_ERR("failed to get 'indicator' atom name: %s", xcb_error(err)); free(err); goto err; } indicators->names[i] = strndup( xcb_get_atom_name_name(atom_name), xcb_get_atom_name_name_length(atom_name)); LOG_DBG("indicator #%zd: %s", i, indicators->names[i]); free(atom_name); } /* e.g. pc+us+inet(evdev)+group(..) */ size_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 >= layouts->count) { LOG_ERR("layout vs group name count mismatch: %zd > %zd", layout_idx + 1, layouts->count); goto err; } char *sym = strdup(fname); layouts->layouts[layout_idx++].symbol = sym; LOG_DBG("layout #%zd: short name: %s", layout_idx - 1, sym); } if (layout_idx != layouts->count) { LOG_ERR("layout vs group name count mismatch: %zd != %zd", layout_idx, layouts->count); goto err; } free(symbols); free(reply); return true; err: free(symbols); free(reply); free_layouts(*layouts); free_indicators(*indicators); return false; } 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) { LOG_ERR("failed to get XKB state: %s", xcb_error(err)); return -1; } int ret = reply->group; free(reply); return ret; } static uint32_t get_indicator_state(xcb_connection_t *conn) { xcb_generic_error_t *err; xcb_xkb_get_indicator_state_cookie_t cookie = xcb_xkb_get_indicator_state( conn, XCB_XKB_ID_USE_CORE_KBD); xcb_xkb_get_indicator_state_reply_t *reply = xcb_xkb_get_indicator_state_reply( conn, cookie, &err); if (err != NULL) { LOG_ERR("failed to get indicator state: %s", xcb_error(err)); free(err); return (uint32_t)-1; } uint32_t state = reply->state; LOG_DBG("indicator state: 0x%08x", state); free(reply); return state; } 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) { LOG_ERR("failed to register for events: %s", xcb_error(err)); return false; } return true; } static bool event_loop(struct module *mod, xcb_connection_t *conn, int xkb_event_base) { const struct bar *bar = mod->bar; struct private *m = mod->private; 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 = mod->abort_fd, .events = POLLIN }, {.fd = xcb_fd, .events = POLLIN | POLLHUP } }; /* Use poll() since xcb_wait_for_events() doesn't return on signals */ if (poll(pfds, sizeof(pfds) / sizeof(pfds[0]), -1) < 0) { if (errno == EINTR) continue; LOG_ERRNO("failed to poll"); break; } if (pfds[0].revents & POLLIN) { ret = true; break; } if (pfds[1].revents & POLLHUP) { LOG_WARN("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) { LOG_WARN("non-XKB event ignored: %d", _evt->response_type); free(_evt); continue; } switch(_evt->pad0) { default: LOG_WARN("unimplemented XKB event: %d", _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; struct indicators indicators; if (!get_layouts_and_indicators(conn, &layouts, &indicators)) { has_error = true; break; } if (current < layouts.count) { mtx_lock(&mod->lock); free_layouts(m->layouts); m->current = current; m->layouts = layouts; m->indicators = indicators; mtx_unlock(&mod->lock); bar->refresh(bar); } else { /* Can happen while transitioning to a new map */ free_layouts(layouts); free_indicators(indicators); } 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) { mtx_lock(&mod->lock); m->current = evt->group; mtx_unlock(&mod->lock); bar->refresh(bar); } break; } case XCB_XKB_MAP_NOTIFY: LOG_WARN("map event unimplemented"); break; case XCB_XKB_INDICATOR_STATE_NOTIFY: { const xcb_xkb_indicator_state_notify_event_t *evt = (const xcb_xkb_indicator_state_notify_event_t *)_evt; #if 0 size_t idx = __builtin_ctz(evt->stateChanged); LOG_ERR("%zu", idx); if (idx < m->indicators.count) LOG_ERR("%s", m->indicators.names[idx]); #endif /* TODO: bit count evt->stateChanged instead */ bool need_refresh = false; for (size_t i = 0; i < m->indicators.count; i++) { bool changed = (evt->stateChanged >> i) & 1; if (!changed) continue; bool enabled = (evt->state >> i) & 1; LOG_DBG("%s: %s", m->indicators.names[i], enabled ? "enabled" : "disabled"); const char *name = m->indicators.names[i]; bool is_caps = strcasecmp(name, "caps lock") == 0; bool is_num = strcasecmp(name, "num lock") == 0; bool is_scroll = strcasecmp(name, "scroll lock") == 0; if (is_caps || is_num || is_scroll) { mtx_lock(&mod->lock); if (is_caps) m->caps_lock = enabled; else if (is_num) m->num_lock = enabled; else if (is_scroll) m->scroll_lock = enabled; mtx_unlock(&mod->lock); need_refresh = true; } } if (need_refresh) bar->refresh(bar); break; } } free(_evt); } } return ret; } static bool talk_to_xkb(struct module *mod, xcb_connection_t *conn) { struct private *m = mod->private; 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; /* Bitmask, one bit for every indicator available */ uint32_t indicator_state = get_indicator_state(conn); if (indicator_state == (uint32_t)-1) return false; struct layouts layouts; struct indicators indicators; if (!get_layouts_and_indicators(conn, &layouts, &indicators)) return false; if (current >= layouts.count) { LOG_ERR("current layout index: %d >= %zd", current, layouts.count); free_layouts(layouts); free_indicators(indicators); return false; } bool caps_lock = false, num_lock = false, scroll_lock = false; /* Lookup initial state of caps-, num- and scroll lock */ for (size_t i = 0; i < indicators.count; i++) { const char *name = indicators.names[i]; bool enabled = (indicator_state >> i) & 1; bool is_caps = strcasecmp(name, "caps lock") == 0; bool is_num = strcasecmp(name, "num lock") == 0; bool is_scroll = strcasecmp(name, "scroll lock") == 0; if (!(is_caps || is_num || is_scroll)) continue; if (is_caps) caps_lock = enabled; else if (is_num) num_lock = enabled; else if (is_scroll) scroll_lock = enabled; LOG_DBG("%s: %s", name, enabled ? "enabled" : "disabled"); } { char buf[512]; size_t idx = 0; for (size_t i = 0; i < layouts.count; i++) { idx += snprintf(&buf[idx], sizeof(buf) - idx, "%s%s (%s)%s", i == m->current ? "*" : "", layouts.layouts[i].name, layouts.layouts[i].symbol, i + 1 < layouts.count ? ", " : ""); } LOG_INFO("layouts: %s, caps-lock:%s, num-lock:%s, scroll-lock:%s", buf, caps_lock ? "on" : "off", num_lock ? "on" : "off", scroll_lock ? "on" : "off"); } mtx_lock(&mod->lock); m->layouts = layouts; m->current = current; m->indicators = indicators; m->caps_lock = caps_lock; m->num_lock = num_lock; m->scroll_lock = scroll_lock; mtx_unlock(&mod->lock); mod->bar->refresh(mod->bar); return event_loop(mod, conn, xkb_event_base); } static int run(struct module *mod) { xcb_connection_t *conn = xcb_connect(NULL, NULL); if (xcb_connection_has_error(conn) > 0) { LOG_ERR("failed to connect to X server"); xcb_disconnect(conn); return EXIT_FAILURE; } int ret = talk_to_xkb(mod, conn) ? EXIT_SUCCESS : EXIT_FAILURE; xcb_disconnect(conn); return ret; } static struct module * xkb_new(struct particle *label) { struct private *m = calloc(1, sizeof(*m)); m->label = label; struct module *mod = module_common_new(); mod->private = m; mod->run = &run; mod->destroy = &destroy; mod->content = &content; mod->description = &description; return mod; } static struct module * from_conf(const struct yml_node *node, struct conf_inherit inherited) { const struct yml_node *c = yml_get_value(node, "content"); return xkb_new(conf_to_particle(c, inherited)); } static bool verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { MODULE_COMMON_ATTRS, }; return conf_verify_dict(chain, node, attrs); } const struct module_iface module_xkb_iface = { .verify_conf = &verify_conf, .from_conf = &from_conf, }; #if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) extern const struct module_iface iface __attribute__((weak, alias("module_xkb_iface"))); #endif