kinit() sets up the XKB extension, after which the server stops sending core MappingNotify to this client, so the refresh branch never fired and strans kept its startup keymap; setxkbmap after launch left every XIM key resolved against the old layout. Select XKB NewKeyboardNotify and MapNotify (with the map parts, or MapNotify is never delivered) and rebuild the keymap on those events instead.
549 lines
12 KiB
C
549 lines
12 KiB
C
#include "dat.h"
|
|
#include "fn.h"
|
|
|
|
#include <errno.h>
|
|
#include <poll.h>
|
|
#include <xcb/xcb.h>
|
|
#include <xcb/xkb.h>
|
|
#include <xkbcommon/xkbcommon.h>
|
|
#include <xkbcommon/xkbcommon-x11.h>
|
|
#include <xcb-imdkit/imdkit.h>
|
|
#include <xcb-imdkit/encoding.h>
|
|
|
|
u32int keymaplookup(struct xkb_state*, uint8_t, u16int);
|
|
|
|
/*
|
|
* One XIM input context. Only PreeditCallbacks clients draw the preedit
|
|
* themselves (cap); the popup shows it for the others. engaged means the
|
|
* engine has seen a real key from this context and owes it a release.
|
|
*/
|
|
typedef struct Ic Ic;
|
|
struct Ic
|
|
{
|
|
Ic *next;
|
|
xcb_im_input_context_t *xic;
|
|
xcb_im_client_t *client;
|
|
int engaged;
|
|
int cap;
|
|
int prestarted;
|
|
int nprerune;
|
|
Caret caret;
|
|
};
|
|
|
|
enum
|
|
{
|
|
Ownerpoll = 200, /* ms between owner checks while a preedit shows */
|
|
};
|
|
|
|
static xcb_connection_t *conn;
|
|
static xcb_im_t *xim;
|
|
static struct xkb_state *kstate;
|
|
static uint8_t xkbevent;
|
|
static xcb_window_t rootwin;
|
|
static Ic *ics;
|
|
static Ic *preowner;
|
|
static Channel *replyc;
|
|
|
|
static char *encs[] = {"COMPOUND_TEXT"};
|
|
static u32int styles[] = {
|
|
XCB_IM_PreeditPosition | XCB_IM_StatusNothing,
|
|
XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing,
|
|
XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
|
|
};
|
|
|
|
static void
|
|
ximlog(char *msg)
|
|
{
|
|
fprint(2, "strans: xim: %s\n", msg);
|
|
}
|
|
|
|
/* Reads the server's keymap; XKB-aware clients get XKB events, not MappingNotify. */
|
|
static int
|
|
kinit(void)
|
|
{
|
|
struct xkb_context *context;
|
|
struct xkb_keymap *keymap;
|
|
struct xkb_state *state;
|
|
int32_t device;
|
|
int ok;
|
|
|
|
keymap = nil;
|
|
state = nil;
|
|
ok = -1;
|
|
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
if(context == nil || !xkb_x11_setup_xkb_extension(conn,
|
|
XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION,
|
|
XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, nil, nil, &xkbevent, nil))
|
|
goto out;
|
|
device = xkb_x11_get_core_keyboard_device_id(conn);
|
|
if(device < 0)
|
|
goto out;
|
|
keymap = xkb_x11_keymap_new_from_device(context, conn, device,
|
|
XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
if(keymap == nil)
|
|
goto out;
|
|
state = xkb_state_new(keymap);
|
|
if(state == nil)
|
|
goto out;
|
|
xkb_state_unref(kstate);
|
|
kstate = state;
|
|
state = nil;
|
|
ok = 0;
|
|
out:
|
|
xkb_state_unref(state);
|
|
xkb_keymap_unref(keymap);
|
|
xkb_context_unref(context);
|
|
return ok;
|
|
}
|
|
|
|
static void
|
|
kwatch(void)
|
|
{
|
|
u16int events, parts;
|
|
|
|
events = XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY |
|
|
XCB_XKB_EVENT_TYPE_MAP_NOTIFY;
|
|
parts = XCB_XKB_MAP_PART_KEY_TYPES | XCB_XKB_MAP_PART_KEY_SYMS |
|
|
XCB_XKB_MAP_PART_MODIFIER_MAP |
|
|
XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS |
|
|
XCB_XKB_MAP_PART_KEY_ACTIONS | XCB_XKB_MAP_PART_VIRTUAL_MODS |
|
|
XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP;
|
|
xcb_xkb_select_events(conn, XCB_XKB_ID_USE_CORE_KBD, events, 0,
|
|
events, parts, parts, nil);
|
|
}
|
|
|
|
static xcb_screen_t*
|
|
getscreen(int scr)
|
|
{
|
|
xcb_screen_iterator_t iter;
|
|
|
|
iter = xcb_setup_roots_iterator(xcb_get_setup(conn));
|
|
for(; iter.rem; scr--, xcb_screen_next(&iter))
|
|
if(scr == 0)
|
|
return iter.data;
|
|
return nil;
|
|
}
|
|
|
|
static int
|
|
translate(xcb_window_t win, int x, int y, Caret *caret)
|
|
{
|
|
xcb_translate_coordinates_cookie_t cookie;
|
|
xcb_translate_coordinates_reply_t *reply;
|
|
|
|
if(win == XCB_NONE)
|
|
return 0;
|
|
cookie = xcb_translate_coordinates(conn, win, rootwin, x, y);
|
|
reply = xcb_translate_coordinates_reply(conn, cookie, nil);
|
|
if(reply == nil || !reply->same_screen){
|
|
free(reply);
|
|
return 0;
|
|
}
|
|
caret->valid = 1;
|
|
caret->x = reply->dst_x;
|
|
caret->y = reply->dst_y;
|
|
caret->h = 0;
|
|
free(reply);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
placebottom(Ic *state, xcb_window_t win)
|
|
{
|
|
xcb_get_geometry_cookie_t gcookie;
|
|
xcb_get_geometry_reply_t *geometry;
|
|
int height;
|
|
|
|
if(win == XCB_NONE)
|
|
return 0;
|
|
gcookie = xcb_get_geometry(conn, win);
|
|
geometry = xcb_get_geometry_reply(conn, gcookie, nil);
|
|
if(geometry == nil)
|
|
return 0;
|
|
height = geometry->height;
|
|
free(geometry);
|
|
if(!translate(win, 0, 0, &state->caret))
|
|
return 0;
|
|
state->caret.y += height;
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* The popup goes at the client's spot location when it sends one, else
|
|
* under the focus window, else under the client window. An unset focus
|
|
* window means the client window, as the XIM spec says.
|
|
*/
|
|
static void
|
|
place(Ic *state)
|
|
{
|
|
const xcb_im_preedit_attr_t *attr;
|
|
xcb_window_t clientwin, focuswin;
|
|
|
|
memset(&state->caret, 0, sizeof state->caret);
|
|
clientwin = xcb_im_input_context_get_client_window(state->xic);
|
|
focuswin = xcb_im_input_context_get_focus_window(state->xic);
|
|
if(focuswin == XCB_NONE)
|
|
focuswin = clientwin;
|
|
if(xcb_im_input_context_get_preedit_attr_mask(state->xic) &
|
|
XCB_XIM_XNSpotLocation_MASK){
|
|
attr = xcb_im_input_context_get_preedit_attr(state->xic);
|
|
if(translate(focuswin, attr->spot_location.x,
|
|
attr->spot_location.y, &state->caret))
|
|
return;
|
|
}
|
|
if(placebottom(state, focuswin))
|
|
return;
|
|
if(clientwin != focuswin)
|
|
placebottom(state, clientwin);
|
|
}
|
|
|
|
/* XIM text goes out as COMPOUND_TEXT; imdkit wraps UTF-8 in ESC%G. */
|
|
static void
|
|
commit(Ic *state, char *s, int nbyte)
|
|
{
|
|
char *wire;
|
|
size_t nwire;
|
|
|
|
if(nbyte == 0)
|
|
return;
|
|
wire = xcb_utf8_to_compound_text(s, nbyte, &nwire);
|
|
if(wire == nil)
|
|
return;
|
|
xcb_im_commit_string(xim, state->xic, XCB_XIM_LOOKUP_CHARS,
|
|
wire, (u32int)nwire, 0);
|
|
free(wire);
|
|
}
|
|
|
|
static void
|
|
clearpreedit(Ic *state)
|
|
{
|
|
xcb_im_preedit_draw_fr_t frame;
|
|
|
|
if(!state->prestarted)
|
|
return;
|
|
memset(&frame, 0, sizeof frame);
|
|
frame.chg_length = state->nprerune;
|
|
frame.status = 1;
|
|
xcb_im_preedit_draw_callback(xim, state->xic, &frame);
|
|
xcb_im_preedit_done_callback(xim, state->xic);
|
|
state->prestarted = 0;
|
|
state->nprerune = 0;
|
|
if(preowner == state)
|
|
preowner = nil;
|
|
xcb_flush(conn);
|
|
}
|
|
|
|
static void
|
|
updatepreedit(Ic *state, Str *pre)
|
|
{
|
|
xcb_im_preedit_draw_fr_t frame;
|
|
u32int feedback[Maxrunes];
|
|
char utf[Maxutf], *wire;
|
|
size_t nwire;
|
|
int i, nbyte;
|
|
|
|
if(!(state->cap & Cclientpreedit))
|
|
return;
|
|
if(pre->n == 0){
|
|
clearpreedit(state);
|
|
return;
|
|
}
|
|
nbyte = stoutf(pre, utf, sizeof utf);
|
|
wire = xcb_utf8_to_compound_text(utf, nbyte, &nwire);
|
|
if(wire == nil)
|
|
return;
|
|
for(i = 0; i < pre->n; i++)
|
|
feedback[i] = XCB_XIM_UNDERLINE;
|
|
if(!state->prestarted){
|
|
xcb_im_preedit_start_callback(xim, state->xic);
|
|
state->prestarted = 1;
|
|
}
|
|
memset(&frame, 0, sizeof frame);
|
|
frame.caret = pre->n;
|
|
frame.chg_length = state->nprerune;
|
|
frame.length_of_preedit_string = (u16int)nwire;
|
|
frame.preedit_string = (uchar*)wire;
|
|
frame.feedback_array.size = pre->n;
|
|
frame.feedback_array.items = feedback;
|
|
xcb_im_preedit_draw_callback(xim, state->xic, &frame);
|
|
state->nprerune = pre->n;
|
|
preowner = state;
|
|
free(wire);
|
|
}
|
|
|
|
static void
|
|
sendrequest(Ic *state, int op, u32int key, u32int mod, Keyres *res)
|
|
{
|
|
Keyreq kr;
|
|
|
|
memset(&kr, 0, sizeof kr);
|
|
kr.owner = state;
|
|
kr.cap = state->cap;
|
|
kr.op = op;
|
|
kr.ks = key;
|
|
kr.mod = ipcmod(mod);
|
|
kr.caret = state->caret;
|
|
kr.reply = replyc;
|
|
chansend(keyc, &kr);
|
|
chanrecv(replyc, res);
|
|
}
|
|
|
|
/* Another frontend may have taken the engine; then our preedit is stale. */
|
|
static void
|
|
checkpreowner(void)
|
|
{
|
|
Keyres res;
|
|
|
|
if(preowner == nil)
|
|
return;
|
|
sendrequest(preowner, Keycap, 0, 0, &res);
|
|
if(!res.eaten)
|
|
clearpreedit(preowner);
|
|
}
|
|
|
|
static void
|
|
keypress(Ic *state, u32int key, u32int mod, Keyres *res)
|
|
{
|
|
if(keymeaningful(key)){
|
|
if(preowner != nil && preowner != state)
|
|
clearpreedit(preowner);
|
|
state->engaged = 1;
|
|
}
|
|
sendrequest(state, Keypress, key, mod, res);
|
|
}
|
|
|
|
static void
|
|
release(Ic *state)
|
|
{
|
|
Keyres res;
|
|
|
|
clearpreedit(state);
|
|
if(!state->engaged)
|
|
return;
|
|
sendrequest(state, Keyrelease, 0, 0, &res);
|
|
state->engaged = 0;
|
|
}
|
|
|
|
/* XIM reset hands the pending preedit back to the client as committed text. */
|
|
static void
|
|
resetic(Ic *state, xcb_im_reset_ic_reply_fr_t *reply)
|
|
{
|
|
Keyres pending, res;
|
|
char utf[Maxutf], *wire;
|
|
size_t nwire;
|
|
int nbyte;
|
|
|
|
if(!state->engaged){
|
|
clearpreedit(state);
|
|
return;
|
|
}
|
|
sendrequest(state, Keycap, 0, 0, &pending);
|
|
sendrequest(state, Keyreset, 0, 0, &res);
|
|
clearpreedit(state);
|
|
if(reply == nil || !pending.eaten || pending.preedit.n == 0)
|
|
return;
|
|
nbyte = stoutf(&pending.preedit, utf, sizeof utf);
|
|
wire = xcb_utf8_to_compound_text(utf, nbyte, &nwire);
|
|
if(wire == nil)
|
|
return;
|
|
reply->committed_string = (uchar*)wire;
|
|
reply->byte_length_of_committed_string = nwire;
|
|
}
|
|
|
|
static void
|
|
kpress(Ic *state, xcb_key_press_event_t *ev)
|
|
{
|
|
Keyres res;
|
|
char buf[Maxutf];
|
|
u32int key;
|
|
int n, wasvalid;
|
|
|
|
key = keymaplookup(kstate, ev->detail, ev->state);
|
|
key = ipckeysym(key, xkb_keysym_to_utf32(key));
|
|
if(keymeaningful(key)){
|
|
wasvalid = state->caret.valid;
|
|
place(state);
|
|
if(state->engaged && wasvalid && !state->caret.valid)
|
|
sendrequest(state, Keycaret, 0, 0, &res);
|
|
}
|
|
keypress(state, key, ev->state, &res);
|
|
n = stoutf(&res.commit, buf, sizeof buf);
|
|
commit(state, buf, n);
|
|
updatepreedit(state, &res.preedit);
|
|
if(!res.eaten)
|
|
xcb_im_forward_event(xim, state->xic, ev);
|
|
xcb_flush(conn);
|
|
}
|
|
|
|
static void
|
|
icunlink(Ic *state)
|
|
{
|
|
Ic **p;
|
|
|
|
for(p = &ics; *p != nil; p = &(*p)->next)
|
|
if(*p == state){
|
|
*p = state->next;
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void
|
|
icfree(void *p)
|
|
{
|
|
Ic *state;
|
|
|
|
state = p;
|
|
/* The owner remains valid until imthread acknowledges its release. */
|
|
release(state);
|
|
icunlink(state);
|
|
free(state);
|
|
}
|
|
|
|
static void
|
|
iccreate(xcb_im_client_t *client, xcb_im_input_context_t *ic)
|
|
{
|
|
Ic *state;
|
|
|
|
state = emalloc(sizeof(Ic));
|
|
state->xic = ic;
|
|
state->client = client;
|
|
if(xcb_im_input_context_get_input_style(ic) ==
|
|
(XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing))
|
|
state->cap = Cclientpreedit;
|
|
state->next = ics;
|
|
ics = state;
|
|
xcb_im_input_context_set_data(ic, state, icfree);
|
|
}
|
|
|
|
static void
|
|
callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic,
|
|
const xcb_im_packet_header_fr_t *hdr, void *frame, void *arg, void *user)
|
|
{
|
|
xcb_key_press_event_t *ev;
|
|
Keyres res;
|
|
Ic *state;
|
|
|
|
USED(im);
|
|
USED(frame);
|
|
USED(user);
|
|
if(hdr->major_opcode == XCB_XIM_DISCONNECT ||
|
|
hdr->major_opcode == XCB_XIM_CLOSE){
|
|
for(state = ics; state != nil; state = state->next)
|
|
if(state->client == client)
|
|
release(state);
|
|
return;
|
|
}
|
|
if(hdr->major_opcode == XCB_XIM_CREATE_IC){
|
|
iccreate(client, ic);
|
|
return;
|
|
}
|
|
if(ic == nil)
|
|
return;
|
|
state = xcb_im_input_context_get_data(ic);
|
|
switch(hdr->major_opcode){
|
|
case XCB_XIM_FORWARD_EVENT:
|
|
ev = arg;
|
|
if(ev != nil && (ev->response_type & ~0x80) == XCB_KEY_PRESS)
|
|
kpress(state, ev);
|
|
break;
|
|
case XCB_XIM_SET_IC_VALUES:
|
|
case XCB_XIM_SET_IC_FOCUS:
|
|
place(state);
|
|
if(state->engaged)
|
|
sendrequest(state, Keycaret, 0, 0, &res);
|
|
break;
|
|
case XCB_XIM_DESTROY_IC:
|
|
case XCB_XIM_UNSET_IC_FOCUS:
|
|
release(state);
|
|
break;
|
|
case XCB_XIM_RESET_IC:
|
|
resetic(state, arg);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int
|
|
ximinit(void)
|
|
{
|
|
xcb_screen_t *screen;
|
|
xcb_window_t win;
|
|
xcb_im_styles_t st;
|
|
xcb_im_encodings_t enc;
|
|
int scr;
|
|
|
|
replyc = chancreate(sizeof(Keyres), 0);
|
|
st.nStyles = nelem(styles);
|
|
st.styles = styles;
|
|
enc.nEncodings = nelem(encs);
|
|
enc.encodings = encs;
|
|
xcb_compound_text_init();
|
|
conn = xcb_connect(nil, &scr);
|
|
if(xcb_connection_has_error(conn)){
|
|
ximlog("cannot connect to X server");
|
|
return -1;
|
|
}
|
|
screen = getscreen(scr);
|
|
if(screen == nil){
|
|
ximlog("cannot find X screen");
|
|
return -1;
|
|
}
|
|
rootwin = screen->root;
|
|
if(kinit() < 0){
|
|
ximlog("cannot read keyboard mapping");
|
|
return -1;
|
|
}
|
|
kwatch();
|
|
win = xcb_generate_id(conn);
|
|
xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root,
|
|
0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
|
|
screen->root_visual, 0, nil);
|
|
xim = xcb_im_create(conn, scr, win, "strans",
|
|
XCB_IM_ALL_LOCALES, &st, nil, nil, &enc,
|
|
XCB_EVENT_MASK_KEY_PRESS, callback, nil);
|
|
if(xim == nil){
|
|
ximlog("cannot create XIM server");
|
|
return -1;
|
|
}
|
|
if(!xcb_im_open_im(xim)){
|
|
ximlog("cannot claim XIM selection");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
ximthread(void *arg)
|
|
{
|
|
struct pollfd pfd;
|
|
xcb_generic_event_t *ev;
|
|
uint8_t type;
|
|
int n;
|
|
|
|
USED(arg);
|
|
threadsetname("xim");
|
|
if(ximinit() < 0)
|
|
die("xim: initialization failed");
|
|
pfd.fd = xcb_get_file_descriptor(conn);
|
|
pfd.events = POLLIN;
|
|
for(;;){
|
|
pfd.revents = 0;
|
|
n = poll(&pfd, 1, preowner != nil ? Ownerpoll : -1);
|
|
if(n < 0 && errno != EINTR)
|
|
break;
|
|
while((ev = xcb_poll_for_event(conn)) != nil){
|
|
type = ev->response_type & ~0x80;
|
|
if(type == xkbevent){
|
|
if(kinit() < 0)
|
|
ximlog("cannot refresh keyboard mapping");
|
|
}else
|
|
xcb_im_filter_event(xim, ev);
|
|
free(ev);
|
|
}
|
|
checkpreowner();
|
|
if(pfd.revents & (POLLERR|POLLHUP|POLLNVAL))
|
|
break;
|
|
}
|
|
if(xcb_connection_has_error(conn))
|
|
die("xim: X server disconnected");
|
|
die("xim: frontend stopped");
|
|
}
|