One xkb_compose_state served the whole session, so a dead key left half typed in one window was completed by the first key in the next. Two IBus contexts through the real processkey(), before this: want length 0, got length 2; byte 0: want <end>, got 0xc3 (text) want 0, got 1 (req.op) Context A pressed dead_acute; context B pressed e, got é, and went down the composed-text branch instead of sending a key. Only COMPOSING is sticky -- xkbcommon starts over by itself after COMPOSED and CANCELLED, which the table in compose_test already pins -- and nothing here ever called xkb_compose_state_reset. That static was also fed from three procs, two of them live at once, with no lock, which xkbcommon forbids. Reaching it needs two focused windows, so it cannot be made to fail on demand and has no test: it goes because the sharing goes, not because anything guards it. So a state per frontend, each starting over when the key comes from another context. All are made in composeinit(), on threadmain, before proccreate, because xkb_compose_state_new refs the table and that ref is a plain increment -- b160: mov (%rdi),%edx b16a: add $0x1,%edx b170: mov %edx,(%rdi) -- so making a state from a shared table on two procs would race in place of the feed. Made before the procs exist they can still share the one table. An owner address that gets reused says so with composedrop: ibus hands out a contexts[] slot again, xim can malloc an Ic at a freed one, and wl's single context, which serves every client in turn, drops at every activate and deactivate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
579 lines
13 KiB
C
579 lines
13 KiB
C
#include "dat.h"
|
|
#include "fn.h"
|
|
|
|
#include <errno.h>
|
|
#include <locale.h>
|
|
#include <poll.h>
|
|
#include <xcb/xcb.h>
|
|
#include <xcb/xcb_aux.h>
|
|
#include <xcb/xkb.h>
|
|
#include <xkbcommon/xkbcommon.h>
|
|
#include <xkbcommon/xkbcommon-names.h>
|
|
#include <xkbcommon/xkbcommon-x11.h>
|
|
#include <xcb-imdkit/imdkit.h>
|
|
#include <xcb-imdkit/encoding.h>
|
|
|
|
/*
|
|
* One XIM input context. Only PreeditCallbacks clients draw the preedit
|
|
* themselves (clientpre); 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 clientpre;
|
|
int prestarted;
|
|
int nprerune;
|
|
Caret caret;
|
|
};
|
|
|
|
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,
|
|
XCB_IM_PreeditPosition | XCB_IM_StatusNone,
|
|
XCB_IM_PreeditCallbacks | XCB_IM_StatusNone,
|
|
XCB_IM_PreeditNothing | XCB_IM_StatusNone,
|
|
};
|
|
|
|
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 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)){
|
|
/* A spot is a baseline: call the line one row above it,
|
|
* so that a popup flipped above it clears the text. */
|
|
state->caret.y -= Fontsz;
|
|
state->caret.h = Fontsz;
|
|
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->clientpre)
|
|
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.clientpre = state->clientpre;
|
|
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);
|
|
}
|
|
|
|
/* Losing the engine commits the pending text; the release hands it back. */
|
|
static void
|
|
release(Ic *state)
|
|
{
|
|
Keyres res;
|
|
char buf[Maxutf];
|
|
int n;
|
|
|
|
if(!state->engaged){
|
|
clearpreedit(state);
|
|
return;
|
|
}
|
|
sendrequest(state, Keyrelease, 0, 0, &res);
|
|
state->engaged = 0;
|
|
n = stoutf(&res.commit, buf, sizeof buf);
|
|
commit(state, buf, n);
|
|
clearpreedit(state);
|
|
xcb_flush(conn);
|
|
}
|
|
|
|
/* XIM reset hands the pending text back to the client as committed text. */
|
|
static void
|
|
resetic(Ic *state, xcb_im_reset_ic_reply_fr_t *reply)
|
|
{
|
|
Keyres res;
|
|
char utf[Maxutf], *wire;
|
|
size_t nwire;
|
|
int nbyte;
|
|
|
|
if(!state->engaged){
|
|
clearpreedit(state);
|
|
return;
|
|
}
|
|
sendrequest(state, Keyreset, 0, 0, &res);
|
|
clearpreedit(state);
|
|
if(reply == nil || res.commit.n == 0)
|
|
return;
|
|
nbyte = stoutf(&res.commit, 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;
|
|
}
|
|
|
|
/* The keysym for an X core key event, with its modifier and group state. */
|
|
static u32int
|
|
keymaplookup(struct xkb_state *state, uchar keycode, u16int corestate)
|
|
{
|
|
static char *modname[] = {
|
|
XKB_MOD_NAME_SHIFT, XKB_MOD_NAME_CAPS, XKB_MOD_NAME_CTRL,
|
|
XKB_MOD_NAME_MOD1, XKB_MOD_NAME_MOD2, XKB_MOD_NAME_MOD3,
|
|
XKB_MOD_NAME_MOD4, XKB_MOD_NAME_MOD5,
|
|
};
|
|
struct xkb_keymap *keymap;
|
|
xkb_mod_index_t index;
|
|
xkb_mod_mask_t mods;
|
|
int i;
|
|
|
|
keymap = xkb_state_get_keymap(state);
|
|
mods = 0;
|
|
for(i = 0; i < nelem(modname); i++){
|
|
if(!(corestate & (1 << i)))
|
|
continue;
|
|
index = xkb_keymap_mod_get_index(keymap, modname[i]);
|
|
if(index != XKB_MOD_INVALID)
|
|
mods |= (xkb_mod_mask_t)1 << index;
|
|
}
|
|
xkb_state_update_mask(state, mods, 0, 0, 0, 0, (corestate >> 13) & 3);
|
|
return xkb_state_key_get_one_sym(state, keycode);
|
|
}
|
|
|
|
static void
|
|
kpress(Ic *state, xcb_key_press_event_t *ev)
|
|
{
|
|
Keyres res;
|
|
char buf[Maxutf], text[Maxutf];
|
|
u32int key, sym;
|
|
int n;
|
|
|
|
sym = keymaplookup(kstate, ev->detail, ev->state);
|
|
if(composekey(Composexim, state, sym, text, sizeof text))
|
|
return;
|
|
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
|
|
if(keymeaningful(key))
|
|
place(state);
|
|
if(text[0] != '\0')
|
|
/* Composed text follows whatever was pending. */
|
|
sendrequest(state, Keyreset, 0, 0, &res);
|
|
else
|
|
keypress(state, key, ev->state, &res);
|
|
n = stoutf(&res.commit, buf, sizeof buf);
|
|
commit(state, buf, n);
|
|
updatepreedit(state, &res.preedit);
|
|
if(text[0] != '\0')
|
|
commit(state, text, strlen(text));
|
|
else 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);
|
|
composedrop(Composexim, 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)
|
|
state->clientpre = 1;
|
|
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 = xcb_aux_get_screen(conn, 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");
|
|
}
|