fix(xim): honor reset and X11 keyboard state

This commit is contained in:
2026-08-14 22:34:37 +09:00
parent 9804c1f55c
commit a637f457f7
10 changed files with 394 additions and 258 deletions

View File

@@ -14,8 +14,8 @@ IBUS_CLIENT_CFLAGS = $(shell pkg-config --cflags ibus-1.0)
IBUS_CLIENT_LIBS = $(shell pkg-config --libs ibus-1.0)
X11_CFLAGS = $(shell pkg-config --cflags x11)
X11_LIBS = $(shell pkg-config --libs x11)
XIM_CFLAGS = $(shell pkg-config --cflags xcb-imdkit xkbcommon)
XIM_LIBS = $(shell pkg-config --libs xcb-imdkit xkbcommon)
XIM_CFLAGS = $(shell pkg-config --cflags xcb-imdkit xkbcommon-x11)
XIM_LIBS = $(shell pkg-config --libs xcb-imdkit xkbcommon-x11)
LIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS)
PROG = unit_test

View File

@@ -31,6 +31,9 @@ static void wirestart(xcb_im_t*, xcb_im_input_context_t*);
static void wiredraw(xcb_im_t*, xcb_im_input_context_t*,
xcb_im_preedit_draw_fr_t*);
static void wiredone(xcb_im_t*, xcb_im_input_context_t*);
static void wireforward(xcb_im_t*, xcb_im_input_context_t*,
xcb_key_press_event_t*);
static void wiresetdata(xcb_im_input_context_t*, void*, xcb_im_free_function);
static xcb_get_geometry_cookie_t wiregeometry(xcb_connection_t*, xcb_drawable_t);
static xcb_get_geometry_reply_t* wiregeometryreply(xcb_connection_t*,
xcb_get_geometry_cookie_t, xcb_generic_error_t**);
@@ -44,6 +47,8 @@ static int wireflush(xcb_connection_t*);
#define xcb_im_preedit_start_callback wirestart
#define xcb_im_preedit_draw_callback wiredraw
#define xcb_im_preedit_done_callback wiredone
#define xcb_im_forward_event wireforward
#define xcb_im_input_context_set_data wiresetdata
#define xcb_im_input_context_get_data(ic) \
(wirebinding(ic) == nil ? nil : wirebinding(ic)->data)
#define xcb_im_input_context_get_client_window(ic) \
@@ -64,6 +69,8 @@ static int wireflush(xcb_connection_t*);
#undef xcb_im_preedit_start_callback
#undef xcb_im_preedit_draw_callback
#undef xcb_im_preedit_done_callback
#undef xcb_im_forward_event
#undef xcb_im_input_context_set_data
#undef xcb_im_input_context_get_data
#undef xcb_im_input_context_get_client_window
#undef xcb_im_input_context_get_focus_window
@@ -82,6 +89,7 @@ enum
Wdraw,
Wdone,
Wcommit,
Wforward,
Maxwire = 8,
};
@@ -189,6 +197,29 @@ wiredone(xcb_im_t *im, xcb_im_input_context_t *ic)
newwire(Wdone, ic);
}
static void
wireforward(xcb_im_t *im, xcb_im_input_context_t *ic,
xcb_key_press_event_t *ev)
{
Wirecall *call;
USED(im);
call = newwire(Wforward, ic);
if(call != nil)
call->keysym = ev->detail;
}
static void
wiresetdata(xcb_im_input_context_t *ic, void *data, xcb_im_free_function freefn)
{
Icbinding *binding;
USED(freefn);
binding = wirebinding(ic);
if(binding != nil)
binding->data = data;
}
static Wirewin*
wirewindow(xcb_window_t win)
{
@@ -306,6 +337,29 @@ wirewin(int n, xcb_window_t win, int x, int y, int h)
return w;
}
static struct xkb_state*
testkeystate(char *layout)
{
struct xkb_context *context;
struct xkb_keymap *keymap;
struct xkb_rule_names names;
struct xkb_state *state;
memset(&names, 0, sizeof names);
names.layout = layout;
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if(context == nil)
return nil;
keymap = xkb_keymap_new_from_names(context, &names,
XKB_KEYMAP_COMPILE_NO_FLAGS);
xkb_context_unref(context);
if(keymap == nil)
return nil;
state = xkb_state_new(keymap);
xkb_keymap_unref(keymap);
return state;
}
void testengineinit(int);
void testenginehandle(Keyreq*);
void* testengineowner(void);
@@ -727,17 +781,16 @@ xim_adapter_placement_updates(struct ct *t)
Ximfix f;
Keyreq req;
Ic state;
xcb_keysym_t key, *oldkmap;
uint8_t oldmin, oldmax, oldper;
struct xkb_state *oldstate;
ic = (xcb_im_input_context_t*)(uintptr)71;
memset(&state, 0, sizeof state);
oldkmap = kmap;
oldmin = minkc;
oldmax = maxkc;
oldper = symsper;
oldstate = kstate;
if(!ximbegin(t, &f, LangJP))
goto cleanup;
kstate = testkeystate("us");
if(!CT_CHECK(t, kstate != nil))
goto cleanup;
wireclear();
state.xic = ic;
state.style = XCB_IM_PreeditPosition | XCB_IM_StatusNothing;
@@ -749,12 +802,8 @@ xim_adapter_placement_updates(struct ct *t)
b->preattr.spot_location.x = 5;
b->preattr.spot_location.y = 6;
wirewin(0, 11, 100, 200, 30);
key = 'n';
kmap = &key;
minkc = maxkc = 8;
symsper = 1;
memset(&ev, 0, sizeof ev);
ev.detail = 8;
ev.detail = 57;
kpress(&state, &ev);
req = nexttrace(t, &f, Keypress, &state);
CT_CHECK(t, req.caret.valid);
@@ -785,10 +834,9 @@ xim_adapter_placement_updates(struct ct *t)
req = nexttrace(t, &f, Keycaret, &state);
CT_CHECK(t, !req.caret.valid);
cleanup:
kmap = oldkmap;
minkc = oldmin;
maxkc = oldmax;
symsper = oldper;
if(kstate != oldstate)
xkb_state_unref(kstate);
kstate = oldstate;
preowner = nil;
ximend(&f);
}
@@ -797,31 +845,27 @@ void
xim_adapter_encoding_negotiation(struct ct *t)
{
xcb_im_packet_header_fr_t hdr;
xcb_im_client_t *a, *b;
xcb_im_client_t *clients[Maxclients+1];
u16int selected;
int i;
a = (xcb_im_client_t*)(uintptr)1;
b = (xcb_im_client_t*)(uintptr)2;
memset(clientenc, 0, sizeof clientenc);
clearencodings();
CT_EQ_SIZE(t, 2, nelem(encs));
CT_EQ_STR(t, "COMPOUND_TEXT", encs[0]);
CT_EQ_STR(t, "UTF8_STRING", encs[1]);
memset(&hdr, 0, sizeof hdr);
hdr.major_opcode = XCB_XIM_ENCODING_NEGOTIATION;
selected = 1;
callback(nil, a, nil, &hdr, nil, &selected, nil);
selected = 0;
callback(nil, b, nil, &hdr, nil, &selected, nil);
CT_EQ_INT(t, Eutf8, getencoding(a));
CT_EQ_INT(t, Ecompound, getencoding(b));
for(i = 0; i < nelem(clients); i++){
clients[i] = (xcb_im_client_t*)(uintptr)(i+1);
selected = i == 0 ? 0 : 1;
callback(nil, clients[i], nil, &hdr, nil, &selected, nil);
}
CT_EQ_INT(t, Ecompound, getencoding(clients[0]));
CT_EQ_INT(t, Eutf8, getencoding(clients[Maxclients]));
hdr.major_opcode = XCB_XIM_DISCONNECT;
callback(nil, a, nil, &hdr, nil, nil, nil);
CT_EQ_INT(t, Ecompound, getencoding(a));
CT_EQ_INT(t, Ecompound, getencoding(b));
hdr.major_opcode = XCB_XIM_CLOSE;
callback(nil, b, nil, &hdr, nil, nil, nil);
CT_EQ_PTR(t, nil, clientenc[0].client);
CT_EQ_PTR(t, nil, clientenc[1].client);
for(i = 0; i < nelem(clients); i++)
callback(nil, clients[i], nil, &hdr, nil, nil, nil);
CT_EQ_PTR(t, nil, clientenc);
}
void
@@ -875,12 +919,7 @@ void
xim_adapter_callback_unicode(struct ct *t)
{
static char *text[] = {
"",
"かなカナ",
"漢字",
"𠀋",
"😀",
"❤️",
"𠀋",
"👩‍💻",
};
Ic state;
@@ -942,16 +981,57 @@ void
xim_adapter_callback_cleanup(struct ct *t)
{
static int opcode[] = {
XCB_XIM_RESET_IC,
XCB_XIM_UNSET_IC_FOCUS,
XCB_XIM_DESTROY_IC,
};
xcb_im_packet_header_fr_t hdr;
xcb_im_reset_ic_reply_fr_t reply;
xcb_key_press_event_t ev;
xcb_im_client_t *client;
xcb_im_input_context_t *ic;
Ximfix f;
Keyres res;
Ic state, *dead;
int i;
ic = (xcb_im_input_context_t*)(uintptr)29;
memset(&state, 0, sizeof state);
if(!ximbegin(t, &f, LangJP))
return;
wireclear();
wirebind(0, ic, &state);
state.xic = ic;
state.style = XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing;
state.cap = Cclientpreedit;
state.encoding = Eutf8;
keypress(&state, 'k', 0, &res);
nexttrace(t, &f, Keypress, &state);
keypress(&state, 'a', 0, &res);
nexttrace(t, &f, Keypress, &state);
updatepreedit(&state, &res.preedit);
memset(&hdr, 0, sizeof hdr);
memset(&reply, 0, sizeof reply);
hdr.major_opcode = XCB_XIM_RESET_IC;
callback(nil, nil, ic, &hdr, nil, &reply, nil);
nexttrace(t, &f, Keycap, &state);
nexttrace(t, &f, Keyreset, &state);
notrace(t, &f);
CT_EQ_INT(t, strlen(""), reply.byte_length_of_committed_string);
CT_EQ_MEM(t, "", reply.committed_string,
reply.byte_length_of_committed_string);
free(reply.committed_string);
CT_CHECK(t, state.engaged);
CT_EQ_PTR(t, &state, testengineowner());
CT_EQ_INT(t, Wdraw, wirecalls[2].op);
CT_EQ_INT(t, Wdone, wirecalls[3].op);
keypress(&state, 'n', 0, &res);
nexttrace(t, &f, Keypress, &state);
checkstr(t, "", &res.preedit);
release(&state);
nexttrace(t, &f, Keyrelease, &state);
preowner = nil;
ximend(&f);
for(i = 0; i < nelem(opcode); i++){
ic = (xcb_im_input_context_t*)(uintptr)(30+i);
wireclear();
@@ -994,6 +1074,24 @@ xim_adapter_callback_cleanup(struct ct *t)
CT_EQ_INT(t, Wdraw, wirecalls[2].op);
CT_EQ_INT(t, Wdone, wirecalls[3].op);
CT_EQ_PTR(t, nil, preowner);
ic = (xcb_im_input_context_t*)(uintptr)42;
wireclear();
wirebind(0, ic, nil);
icinstall(nil, ic, nil);
CT_EQ_PTR(t, &passthrough, icbindings[0].data);
memset(&ev, 0, sizeof ev);
ev.response_type = XCB_KEY_PRESS;
ev.detail = 38;
hdr.major_opcode = XCB_XIM_FORWARD_EVENT;
callback(nil, nil, ic, &hdr, nil, &ev, nil);
CT_EQ_INT(t, 1, nwirecalls);
CT_EQ_INT(t, Wforward, wirecalls[0].op);
CT_EQ_UINT(t, ev.detail, wirecalls[0].keysym);
ev.response_type = XCB_KEY_RELEASE;
callback(nil, nil, ic, &hdr, nil, &ev, nil);
CT_EQ_INT(t, 2, nwirecalls);
CT_EQ_INT(t, Wforward, wirecalls[1].op);
}
void

View File

@@ -614,46 +614,6 @@ waitcommit(Display *dpy, XIC ic, char *commit, size_t cap)
return 0;
}
static int
rawtakeover(void)
{
unsigned char req[Ipcreqsz];
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp;
struct timeval tv;
int fd;
fd = ipcconnect();
if(fd < 0)
return fail("connect raw IPC owner: %s", strerror(errno));
tv.tv_sec = Eventtimeout / 1000;
tv.tv_usec = (Eventtimeout % 1000) * 1000;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
ipcpackreq(req, 0, 0, 'z');
if(ipcsend(fd, req, sizeof req) < 0 ||
ipcreadresp(fd, 0, commit, sizeof commit,
preedit, sizeof preedit, &resp) < 0){
close(fd);
return fail("raw IPC ownership transfer: %s", strerror(errno));
}
return fd;
}
static int
waitclear(Display *dpy, Prelog *p, int empty, int done)
{
int64_t deadline;
deadline = nowms() + Eventtimeout;
while(leftms(deadline) > 0){
pump(dpy, leftms(deadline));
if(p->empty >= empty && p->done >= done)
return 1;
}
return fail("timed out waiting for empty DRAW and DONE");
}
static int
waitpreedit(Display *dpy, Prelog *p)
{
@@ -692,7 +652,6 @@ testcallbacks(Display *dpy, XIM im, Window win)
char commit[Ipcfieldmax+1], *reset;
Prelog log;
XIC ic;
int fd, empty, done;
memset(&log, 0, sizeof log);
ic = callbackic(im, win, &log);
@@ -730,42 +689,33 @@ testcallbacks(Display *dpy, XIM im, Window win)
return fail("bad initial callback sequence: start=%d draw=%d empty=%d done=%d ordered=%d bad=%d",
start, draw, empty, done, ordered, bad);
}
empty = log.empty;
done = log.done;
fd = rawtakeover();
if(fd < 0){
XDestroyIC(ic);
return 0;
}
if(!waitclear(dpy, &log, empty + 1, done + 1)){
close(fd);
XDestroyIC(ic);
return 0;
}
/* Leave the live IC time to expose a delayed duplicate transfer clear. */
pump(dpy, 100);
if(log.empty != empty + 1 || log.done != done + 1 ||
log.doneafterempty != 1 || log.badorder != 0){
int gotempty, gotdone, ordered;
gotempty = log.empty - empty;
gotdone = log.done - done;
ordered = log.doneafterempty;
close(fd);
XDestroyIC(ic);
return fail("owner loss callback counts: empty=%d done=%d ordered=%d",
gotempty, gotdone, ordered);
}
reset = Xutf8ResetIC(ic);
if(reset != NULL)
XFree(reset);
if(reset == NULL || strcmp(reset, "") != 0){
if(reset != NULL)
XFree(reset);
XDestroyIC(ic);
return fail("ResetIC did not return pending preedit");
}
XFree(reset);
if(!sendkey(dpy, ic, win, XK_r, 0, commit, sizeof commit) ||
!sendkey(dpy, ic, win, XK_k, 0, commit, sizeof commit) ||
!sendkey(dpy, ic, win, XK_Return, 0, commit, sizeof commit)){
XDestroyIC(ic);
return 0;
}
if(commit[0] == '\0' && !waitcommit(dpy, ic, commit, sizeof commit)){
XDestroyIC(ic);
return fail("input context did not compose after ResetIC");
}
if(strcmp(commit, "") != 0){
XDestroyIC(ic);
return fail("post-reset composition did not contain 가");
}
XUnsetICFocus(ic);
XDestroyIC(ic);
pump(dpy, 100);
close(fd);
pump(dpy, 100);
if(log.empty != empty + 1 || log.done != done + 1)
return fail("late lifecycle produced duplicate clear callbacks");
if(log.badorder != 0)
return fail("bad callback order after ResetIC");
return 1;
}

View File

@@ -4,18 +4,61 @@
#include <stdlib.h>
#include <string.h>
#include <xcb-imdkit/encoding.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-keysyms.h>
uint32_t keymaplookup(const uint32_t*, int, uint16_t);
uint32_t keymaplookup(struct xkb_state*, uint8_t, uint16_t);
char *ximcompound(const char*, size_t, size_t*);
enum
{
ShiftMask = 1<<0,
LockMask = 1<<1,
Mod2Mask = 1<<4,
Mod5Mask = 1<<7,
Group1 = 1<<13,
};
static struct xkb_state*
keystate(const char *layout)
{
struct xkb_context *context;
struct xkb_keymap *keymap;
struct xkb_rule_names names;
struct xkb_state *state;
memset(&names, 0, sizeof names);
names.layout = layout;
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
assert(context != NULL);
keymap = xkb_keymap_new_from_names(context, &names,
XKB_KEYMAP_COMPILE_NO_FLAGS);
assert(keymap != NULL);
state = xkb_state_new(keymap);
assert(state != NULL);
xkb_keymap_unref(keymap);
xkb_context_unref(context);
return state;
}
static void
checkkeys(void)
{
struct xkb_state *state;
state = keystate("de,ru");
assert(keymaplookup(state, 38, 0) == XKB_KEY_a);
assert(keymaplookup(state, 38, ShiftMask) == XKB_KEY_A);
assert(keymaplookup(state, 38, LockMask) == XKB_KEY_A);
assert(keymaplookup(state, 38, LockMask|ShiftMask) == XKB_KEY_a);
assert(keymaplookup(state, 26, Mod5Mask) == XKB_KEY_EuroSign);
assert(keymaplookup(state, 26, Group1) == XKB_KEY_Cyrillic_u);
assert(keymaplookup(state, 87, 0) == XKB_KEY_KP_End);
assert(keymaplookup(state, 87, Mod2Mask) == XKB_KEY_KP_1);
xkb_state_unref(state);
assert(keymaplookup(NULL, 38, 0) == XKB_KEY_NoSymbol);
}
static void
checktext(const char *s)
{
@@ -53,11 +96,7 @@ checkcorpus(void)
{
static const char *text[] = {
"한글",
"かなカナ",
"漢字",
"𠀋",
"😀",
"❤️",
"👩‍💻",
};
size_t i;
@@ -69,28 +108,9 @@ checkcorpus(void)
int
main(void)
{
static const uint32_t letters[] = {
XKB_KEY_a, XKB_KEY_A, XKB_KEY_Cyrillic_ef, XKB_KEY_Cyrillic_EF,
};
static const uint32_t inferred[] = { XKB_KEY_a, XKB_KEY_NoSymbol };
static const uint32_t punctuation[] = { XKB_KEY_1, XKB_KEY_exclam };
static const uint32_t missing[] = {
XKB_KEY_a, XKB_KEY_A, XKB_KEY_NoSymbol, XKB_KEY_NoSymbol,
};
xcb_compound_text_init();
assert(keymaplookup(letters, 4, 0) == XKB_KEY_a);
assert(keymaplookup(letters, 4, ShiftMask) == XKB_KEY_A);
assert(keymaplookup(letters, 4, LockMask) == XKB_KEY_A);
assert(keymaplookup(letters, 4, LockMask|ShiftMask) == XKB_KEY_a);
assert(keymaplookup(letters, 4, Group1) == XKB_KEY_Cyrillic_ef);
assert(keymaplookup(letters, 4, Group1|ShiftMask) == XKB_KEY_Cyrillic_EF);
assert(keymaplookup(inferred, 2, ShiftMask) == XKB_KEY_A);
assert(keymaplookup(punctuation, 2, LockMask) == XKB_KEY_1);
assert(keymaplookup(punctuation, 2, LockMask|ShiftMask) == XKB_KEY_exclam);
assert(keymaplookup(missing, 4, Group1) == XKB_KEY_a);
assert(keymaplookup(NULL, 0, 0) == XKB_KEY_NoSymbol);
checkkeys();
checkcorpus();
checktext("A😀한");
checkutf8run();