forked from external/yambar
454 lines
12 KiB
C
454 lines
12 KiB
C
#include "xkb.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
|
|
#include <poll.h>
|
|
|
|
#include <xcb/xcb.h>
|
|
#include <xcb/xkb.h>
|
|
|
|
#define LOG_MODULE "xkb"
|
|
#include "../log.h"
|
|
#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);
|
|
module_default_destroy(mod);
|
|
}
|
|
|
|
static struct exposable *
|
|
content(struct module *mod)
|
|
{
|
|
const struct private *m = mod->private;
|
|
|
|
struct tag_set tags = {
|
|
.tags = (struct tag *[]){
|
|
tag_new_string(mod, "name", m->layouts.layouts[m->current].name),
|
|
tag_new_string(mod, "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) {
|
|
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 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) {
|
|
LOG_ERR("failed to get group names and symbols (%d)", err->error_code);
|
|
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]);
|
|
|
|
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 atom name (%d)", err->error_code);
|
|
free(err);
|
|
goto err;
|
|
}
|
|
|
|
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) {
|
|
LOG_ERR("failed to get atom name (%d)", 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) {
|
|
LOG_ERR("layout vs group name count mismatch: %zd > %zd",
|
|
layout_idx + 1, ret.count);
|
|
goto err;
|
|
}
|
|
|
|
char *sym = strdup(fname);
|
|
ret.layouts[layout_idx++].symbol = sym;
|
|
}
|
|
|
|
if (layout_idx != ret.count) {
|
|
LOG_ERR("layout vs group name count mismatch: %zd != %zd",
|
|
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) {
|
|
LOG_ERR("failed to get XKB state (%d)", err->error_code);
|
|
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) {
|
|
LOG_ERR("failed to register for events (%d)", err->error_code);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
event_loop(struct module_run_context *ctx, xcb_connection_t *conn,
|
|
int xkb_event_base)
|
|
{
|
|
struct private *m = ctx->module->private;
|
|
const struct bar *bar = ctx->module->bar;
|
|
|
|
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 = ctx->abort_fd, .events = POLLIN },
|
|
{.fd = xcb_fd, .events = POLLIN | POLLHUP }
|
|
};
|
|
|
|
/* Use poll() since xcb_wait_for_events() doesn't return on signals */
|
|
poll(pfds, sizeof(pfds) / sizeof(pfds[0]), -1);
|
|
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 = get_layouts(conn);
|
|
if (layouts.count == -1) {
|
|
has_error = true;
|
|
break;
|
|
}
|
|
|
|
if (current < layouts.count) {
|
|
free_layouts(m->layouts);
|
|
m->layouts = layouts;
|
|
m->current = current;
|
|
bar->refresh(bar);
|
|
} else {
|
|
/* Can happen while transitioning to a new map */
|
|
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) {
|
|
m->current = evt->group;
|
|
bar->refresh(bar);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case XCB_XKB_MAP_NOTIFY:
|
|
LOG_WARN("map event unimplemented");
|
|
break;
|
|
|
|
case XCB_XKB_INDICATOR_STATE_NOTIFY:
|
|
LOG_WARN("indicator state event unimplemented");
|
|
break;
|
|
}
|
|
|
|
free(_evt);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static bool
|
|
talk_to_xkb(struct module_run_context *ctx, xcb_connection_t *conn)
|
|
{
|
|
struct private *m = ctx->module->private;
|
|
|
|
if (!xkb_enable(conn))
|
|
goto err;
|
|
|
|
if (!register_for_events(conn))
|
|
goto err;
|
|
|
|
int xkb_event_base = get_xkb_event_base(conn);
|
|
if (xkb_event_base == -1)
|
|
goto err;
|
|
|
|
int current = get_current_layout(conn);
|
|
if (current == -1)
|
|
goto err;
|
|
|
|
struct layouts layouts = get_layouts(conn);
|
|
if (layouts.count == -1)
|
|
goto err;
|
|
|
|
if (current >= layouts.count) {
|
|
LOG_ERR("current layout index: %d >= %zd", current, layouts.count);
|
|
free_layouts(layouts);
|
|
goto err;
|
|
}
|
|
|
|
m->layouts = layouts;
|
|
m->current = current;
|
|
|
|
module_signal_ready(ctx);
|
|
return event_loop(ctx, conn, xkb_event_base);
|
|
|
|
err:
|
|
module_signal_ready(ctx);
|
|
return false;
|
|
}
|
|
|
|
static int
|
|
run(struct module_run_context *ctx)
|
|
{
|
|
xcb_connection_t *conn = xcb_connect(NULL, NULL);
|
|
if (conn == NULL) {
|
|
LOG_ERR("failed to connect to X server");
|
|
module_signal_ready(ctx);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
int ret = talk_to_xkb(ctx, 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;
|
|
m->current = 0;
|
|
m->layouts.count = 0;
|
|
m->layouts.layouts = NULL;
|
|
|
|
struct module *mod = module_common_new();
|
|
mod->private = m;
|
|
mod->run = &run;
|
|
mod->destroy = &destroy;
|
|
mod->content = &content;
|
|
return mod;
|
|
}
|