fix: repair XIM context lifecycle

This commit is contained in:
2026-08-12 15:56:43 +09:00
parent 103f2ec448
commit 1295480858
6 changed files with 181 additions and 43 deletions

View File

@@ -49,6 +49,7 @@ docker-check: docker
$(DOCKER_IMAGE) make check $(DOCKER_IMAGE) make check
check: verify-map test check: verify-map test
$(MAKE) -C xim check
test: test:
$(MAKE) -C tests check TESTARGS="$(TESTARGS)" $(MAKE) -C tests check TESTARGS="$(TESTARGS)"

38
tests/xim_test.c Normal file
View File

@@ -0,0 +1,38 @@
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <xkbcommon/xkbcommon-keysyms.h>
#include "../xim/keymap.h"
enum
{
ShiftMask = 1<<0,
LockMask = 1<<1,
Group1 = 1<<13,
};
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,
};
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);
return 0;
}

View File

@@ -6,6 +6,7 @@ PROG = strans-xim
SRCS = $(wildcard *.c) SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o) OBJS = $(SRCS:.c=.o)
TEST = xim_test
all: $(PROG) all: $(PROG)
@@ -14,7 +15,13 @@ $(PROG): $(OBJS) ../ipc.c ../ipc.h
$(OBJS): ../ipc.h $(OBJS): ../ipc.h
clean: $(TEST): ../tests/xim_test.c keymap.c keymap.h
rm -f $(OBJS) $(PROG) $(CC) $(CFLAGS) -o $@ ../tests/xim_test.c keymap.c $(XIM_LIBS)
.PHONY: all clean check: $(TEST)
./$(TEST)
clean:
rm -f $(OBJS) $(PROG) $(TEST)
.PHONY: all check clean

37
xim/keymap.c Normal file
View File

@@ -0,0 +1,37 @@
#include <xkbcommon/xkbcommon.h>
#include "keymap.h"
enum
{
ShiftMask = 1<<0,
LockMask = 1<<1,
GroupShift = 13,
GroupMask = 3,
};
uint32_t
keymaplookup(const uint32_t *syms, int nsyms, uint16_t state)
{
uint32_t lo, hi, lower, upper;
int col, group, shift;
if(syms == NULL || nsyms <= 0)
return XKB_KEY_NoSymbol;
group = (state >> GroupShift) & GroupMask;
col = group * 2;
if(col >= nsyms || syms[col] == XKB_KEY_NoSymbol)
col = 0;
lo = syms[col];
if(lo == XKB_KEY_NoSymbol)
return XKB_KEY_NoSymbol;
hi = col + 1 < nsyms ? syms[col + 1] : XKB_KEY_NoSymbol;
lower = xkb_keysym_to_lower(lo);
upper = xkb_keysym_to_upper(lo);
if(hi == XKB_KEY_NoSymbol)
hi = lower != upper ? upper : lo;
shift = (state & ShiftMask) != 0;
/* Lock reverses Shift only for keys with an alphabetic case pair. */
if((state & LockMask) && lower != upper)
shift = !shift;
return shift ? hi : lo;
}

8
xim/keymap.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef STRANS_XIM_KEYMAP_H
#define STRANS_XIM_KEYMAP_H
#include <stdint.h>
uint32_t keymaplookup(const uint32_t*, int, uint16_t);
#endif

View File

@@ -9,15 +9,21 @@
#include <xcb-imdkit/imdkit.h> #include <xcb-imdkit/imdkit.h>
#include <xcb-imdkit/encoding.h> #include <xcb-imdkit/encoding.h>
#include "ipc.h" #include "ipc.h"
#include "keymap.h"
typedef struct Ic Ic;
struct Ic
{
int fd;
};
static xcb_connection_t *conn; static xcb_connection_t *conn;
static xcb_im_t *xim; static xcb_im_t *xim;
static int srvfd = -1;
static xcb_keysym_t *kmap; static xcb_keysym_t *kmap;
static uint8_t minkc, maxkc; static uint8_t minkc, maxkc;
static uint8_t symsper; static uint8_t symsper;
static char *encs[] = {"COMPOUND_TEXT", "en_US.UTF-8", ""}; static char *encs[] = {"COMPOUND_TEXT"};
static uint32_t styles[] = { static uint32_t styles[] = {
XCB_IM_PreeditNothing | XCB_IM_StatusNothing, XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
XCB_IM_PreeditNone | XCB_IM_StatusNone, XCB_IM_PreeditNone | XCB_IM_StatusNone,
@@ -37,7 +43,7 @@ kinit(void)
xcb_get_keyboard_mapping_reply_t *r; xcb_get_keyboard_mapping_reply_t *r;
const xcb_setup_t *setup; const xcb_setup_t *setup;
xcb_keysym_t *syms; xcb_keysym_t *syms;
int i, n; int n;
setup = xcb_get_setup(conn); setup = xcb_get_setup(conn);
if(setup == NULL) if(setup == NULL)
@@ -50,26 +56,22 @@ kinit(void)
die("keyboard mapping failed"); die("keyboard mapping failed");
symsper = r->keysyms_per_keycode; symsper = r->keysyms_per_keycode;
n = xcb_get_keyboard_mapping_keysyms_length(r); n = xcb_get_keyboard_mapping_keysyms_length(r);
kmap = malloc(n * sizeof(xcb_keysym_t)); syms = malloc(n * sizeof(xcb_keysym_t));
if(kmap == NULL) if(syms == NULL)
die("malloc failed"); die("malloc failed");
syms = xcb_get_keyboard_mapping_keysyms(r); memcpy(syms, xcb_get_keyboard_mapping_keysyms(r),
for(i = 0; i < n; i++) n * sizeof(xcb_keysym_t));
kmap[i] = syms[i]; free(kmap);
kmap = syms;
free(r); free(r);
} }
static uint32_t static uint32_t
kget(uint8_t kc, uint16_t state) kget(uint8_t kc, uint16_t state)
{ {
int col;
if(kmap == NULL || kc < minkc || kc > maxkc) if(kmap == NULL || kc < minkc || kc > maxkc)
return 0; return 0;
col = (state & Mshift) ? 1 : 0; return keymaplookup(kmap + (kc - minkc) * symsper, symsper, state);
if(col >= symsper)
col = 0;
return kmap[(kc - minkc) * symsper + col];
} }
static xcb_screen_t* static xcb_screen_t*
@@ -106,21 +108,21 @@ commit(xcb_im_input_context_t *ic, char *s, int len)
} }
static void static void
srvclose(void) srvclose(Ic *state)
{ {
if(srvfd < 0) if(state == NULL || state->fd < 0)
return; return;
close(srvfd); close(state->fd);
srvfd = -1; state->fd = -1;
} }
static int static int
srvconnect(void) srvconnect(Ic *state)
{ {
struct sockaddr_un addr; struct sockaddr_un addr;
int fd; int fd;
if(srvfd >= 0) if(state->fd >= 0)
return 0; return 0;
fd = socket(AF_UNIX, SOCK_STREAM, 0); fd = socket(AF_UNIX, SOCK_STREAM, 0);
if(fd < 0) if(fd < 0)
@@ -132,25 +134,25 @@ srvconnect(void)
close(fd); close(fd);
return -1; return -1;
} }
srvfd = fd; state->fd = fd;
return 0; return 0;
} }
static int static int
readresp(xcb_im_input_context_t *ic) readresp(Ic *state, xcb_im_input_context_t *ic)
{ {
char buf[Ipcfieldmax+1]; char buf[Ipcfieldmax+1];
Ipcresp resp; Ipcresp resp;
if(ipcreadresp(srvfd, 0, buf, sizeof buf, NULL, 0, &resp) < 0) if(ipcreadresp(state->fd, 0, buf, sizeof buf, NULL, 0, &resp) < 0)
return -1; return -1;
if(buf[0] != '\0') if(resp.ncommit > 0)
commit(ic, buf, strlen(buf)); commit(ic, buf, resp.ncommit);
return resp.eaten; return resp.eaten;
} }
static void static void
kpress(xcb_im_input_context_t *ic, xcb_key_press_event_t *ev) kpress(Ic *state, xcb_im_input_context_t *ic, xcb_key_press_event_t *ev)
{ {
unsigned char buf[Ipcreqsz]; unsigned char buf[Ipcreqsz];
uint32_t key, rune; uint32_t key, rune;
@@ -166,10 +168,10 @@ kpress(xcb_im_input_context_t *ic, xcb_key_press_event_t *ev)
key = rune; key = rune;
ipcpackreq(buf, 0, ev->state, key); ipcpackreq(buf, 0, ev->state, key);
eaten = 0; eaten = 0;
if(srvconnect() == 0){ if(srvconnect(state) == 0){
if(ipcsend(srvfd, buf, sizeof buf) < 0 || if(ipcsend(state->fd, buf, sizeof buf) < 0 ||
(eaten = readresp(ic)) < 0){ (eaten = readresp(state, ic)) < 0){
srvclose(); srvclose(state);
eaten = 0; eaten = 0;
} }
} }
@@ -179,15 +181,43 @@ kpress(xcb_im_input_context_t *ic, xcb_key_press_event_t *ev)
} }
static void static void
reset(xcb_im_input_context_t *ic) reset(Ic *state, xcb_im_input_context_t *ic, int release)
{ {
unsigned char buf[Ipcreqsz]; unsigned char buf[Ipcreqsz];
ipcpackreset(buf, 0); if(state == NULL || state->fd < 0)
if(srvconnect() < 0)
return; return;
if(ipcsend(srvfd, buf, sizeof buf) < 0 || readresp(ic) < 0) ipcpackreset(buf, 0);
srvclose(); if(ipcsend(state->fd, buf, sizeof buf) < 0 ||
readresp(state, ic) < 0)
release = 1;
if(release)
srvclose(state);
}
static void
icfree(void *p)
{
Ic *state;
state = p;
/* imdkit frees this data on context destruction and client loss. */
srvclose(state);
free(state);
}
static void
iccreate(xcb_im_input_context_t *ic)
{
Ic *state;
if(ic == NULL || xcb_im_input_context_get_data(ic) != NULL)
return;
state = calloc(1, sizeof *state);
if(state == NULL)
return;
state->fd = -1;
xcb_im_input_context_set_data(ic, state, icfree);
} }
static void static void
@@ -195,20 +225,33 @@ 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) const xcb_im_packet_header_fr_t *hdr, void *frame, void *arg, void *user)
{ {
xcb_key_press_event_t *ev; xcb_key_press_event_t *ev;
Ic *state;
(void)im; (void)im;
(void)client; (void)client;
(void)frame; (void)frame;
(void)user; (void)user;
if(hdr->major_opcode == XCB_XIM_CREATE_IC){
iccreate(ic);
return;
}
if(ic == NULL)
return;
state = xcb_im_input_context_get_data(ic);
if(state == NULL)
return;
switch(hdr->major_opcode){ switch(hdr->major_opcode){
case XCB_XIM_FORWARD_EVENT: case XCB_XIM_FORWARD_EVENT:
ev = arg; ev = arg;
if(ev != NULL && (ev->response_type & ~0x80) == XCB_KEY_PRESS) if(ev != NULL && (ev->response_type & ~0x80) == XCB_KEY_PRESS)
kpress(ic, ev); kpress(state, ic, ev);
break;
case XCB_XIM_RESET_IC:
reset(state, ic, 0);
break; break;
case XCB_XIM_UNSET_IC_FOCUS: case XCB_XIM_UNSET_IC_FOCUS:
reset(ic); reset(state, ic, 1);
break; break;
} }
} }
@@ -224,7 +267,7 @@ ximinit(void)
st.nStyles = 2; st.nStyles = 2;
st.styles = styles; st.styles = styles;
enc.nEncodings = 3; enc.nEncodings = 1;
enc.encodings = encs; enc.encodings = encs;
xcb_compound_text_init(); xcb_compound_text_init();
conn = xcb_connect(NULL, &scr); conn = xcb_connect(NULL, &scr);
@@ -247,15 +290,19 @@ int
main(void) main(void)
{ {
xcb_generic_event_t *ev; xcb_generic_event_t *ev;
uint8_t type;
ximinit(); ximinit();
for(;;){ for(;;){
ev = xcb_wait_for_event(conn); ev = xcb_wait_for_event(conn);
if(ev == NULL) if(ev == NULL)
break; break;
type = ev->response_type & ~0x80;
if(type == XCB_MAPPING_NOTIFY)
kinit();
else
xcb_im_filter_event(xim, ev); xcb_im_filter_event(xim, ev);
free(ev); free(ev);
} }
srvclose();
return 0; return 0;
} }