xim: drop what no client uses; poll for owner loss like IBus

XIM text always went out through imdkit's COMPOUND_TEXT converter,
which already wraps every UTF-8 string in ESC%G; the UTF8_STRING
negotiation, the per-client encoding list, ximtext.c's fallback and its
wrapper never changed a byte on the wire. The spot location is honoured
for every style now (GTK's XIM module sends it with PreeditCallbacks)
and an unset focus window means the client window, as the spec says,
so those clients get the popup at the caret. readattrs/place kept four
transient fields to pass values between them; ximclose tore down state
right before die(); the OOM passthrough context was a third policy for
one small calloc where emalloc dies like everything else. Both frontends
now poll for engine-owner loss only while a preedit shows instead of
XIM waking on a pipe the engine had to know about; ibus stops waking
five times a second when idle. keymeaningful() is the engine's own
predicate. The standalone xim_test moved into the unit suite, so
xim/Makefile is gone.
This commit is contained in:
2026-08-16 16:16:31 +09:00
parent 9080bb833c
commit a70cfc42e3
15 changed files with 240 additions and 764 deletions

View File

@@ -1,21 +0,0 @@
CC = cc
PKG_CONFIG ?= pkg-config
CFLAGS ?= -O2
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xkbcommon-x11)
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xkbcommon-x11)
TEST = xim_test
all: $(TEST)
$(TEST): ../tests/xim_test.c keymap.c ximtext.c
$(CC) $(CPPFLAGS) -I.. $(XIM_CFLAGS) -Wall -Wextra $(CFLAGS) \
$(LDFLAGS) -o $@ ../tests/xim_test.c keymap.c ximtext.c \
$(XIM_LIBS) $(LDLIBS)
check: $(TEST)
./$(TEST)
clean:
rm -f main.o strans-xim $(TEST)
.PHONY: all check clean

View File

@@ -3,6 +3,7 @@
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-names.h>
/* The keysym for an X core key event, with its modifier and group state. */
uint32_t
keymaplookup(struct xkb_state *state, uint8_t keycode, uint16_t corestate)
{
@@ -21,15 +22,13 @@ keymaplookup(struct xkb_state *state, uint8_t keycode, uint16_t corestate)
xkb_mod_mask_t mods;
int i;
if(state == NULL)
return XKB_KEY_NoSymbol;
keymap = xkb_state_get_keymap(state);
mods = 0;
for(i = 0; i < (int)(sizeof modname / sizeof modname[0]); i++){
if(!(corestate & (1U << i)))
continue;
index = xkb_keymap_mod_get_index(keymap, modname[i]);
if(index != XKB_MOD_INVALID && index < 8 * sizeof mods)
if(index != XKB_MOD_INVALID)
mods |= (xkb_mod_mask_t)1 << index;
}
xkb_state_update_mask(state, mods, 0, 0, 0, 0,

464
xim/xim.c
View File

@@ -2,9 +2,7 @@
#include "fn.h"
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include <xcb/xcb.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-x11.h>
@@ -12,8 +10,12 @@
#include <xcb-imdkit/encoding.h>
u32int keymaplookup(struct xkb_state*, uint8_t, u16int);
char *ximcompound(const char*, size_t, size_t*);
/*
* 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
{
@@ -21,188 +23,31 @@ struct Ic
xcb_im_input_context_t *xic;
xcb_im_client_t *client;
int engaged;
u32int style;
int cap;
int encoding;
int prestarted;
int nprerune;
xcb_window_t clientwin;
xcb_window_t focuswin;
xcb_point_t spot;
int hasspot;
Caret caret;
};
typedef struct Clientenc Clientenc;
struct Clientenc
{
Clientenc *next;
xcb_im_client_t *client;
int encoding;
};
enum
{
Ecompound,
Eutf8,
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 xcb_window_t rootwin;
static int opened;
static Ic *ics;
static Ic *preowner;
static Ic passthrough;
static Clientenc *clientenc;
static Channel *replyc;
static int wakefd[2] = {-1, -1};
static char *encs[] = {"COMPOUND_TEXT", "UTF8_STRING"};
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 int stylecaps[] = {0, Cclientpreedit, 0};
static int
stylecap(u32int style, int *cap)
{
int i;
for(i = 0; i < nelem(styles); i++)
if(styles[i] == style){
*cap = stylecaps[i];
return 1;
}
*cap = 0;
return 0;
}
static int
wakeinit(void)
{
int fd[2];
int flags, i;
if(wakefd[0] >= 0)
return 0;
if(pipe(fd) < 0)
return -1;
for(i = 0; i < nelem(fd); i++){
flags = fcntl(fd[i], F_GETFL);
if(flags < 0 || fcntl(fd[i], F_SETFL,
flags | O_NONBLOCK) < 0)
goto fail;
flags = fcntl(fd[i], F_GETFD);
if(flags < 0 || fcntl(fd[i], F_SETFD,
flags | FD_CLOEXEC) < 0)
goto fail;
}
wakefd[0] = fd[0];
wakefd[1] = fd[1];
return 0;
fail:
close(fd[0]);
close(fd[1]);
return -1;
}
/* imthread only leaves a wake token; XIM ownership stays on the XIM thread. */
void
ximownernotify(void)
{
uchar token;
int n;
if(wakefd[1] < 0)
return;
token = 0;
do
n = write(wakefd[1], &token, 1);
while(n < 0 && errno == EINTR);
/* EAGAIN means a pending byte will already wake the XIM poll loop. */
}
static int
wakedrain(void)
{
uchar buf[32];
int n, total;
total = 0;
for(;;){
n = read(wakefd[0], buf, sizeof buf);
if(n > 0){
total += n;
continue;
}
if(n < 0 && errno == EINTR)
continue;
break;
}
return total;
}
static int
getencoding(xcb_im_client_t *client)
{
Clientenc *p;
for(p = clientenc; p != nil; p = p->next)
if(p->client == client)
return p->encoding;
return Ecompound;
}
static int
setencoding(xcb_im_client_t *client, int encoding)
{
Clientenc *p;
if(client == nil)
return -1;
for(p = clientenc; p != nil; p = p->next)
if(p->client == client){
p->encoding = encoding;
return 0;
}
p = calloc(1, sizeof *p);
if(p == nil)
return -1;
p->client = client;
p->encoding = encoding;
p->next = clientenc;
clientenc = p;
return 0;
}
static void
clearencoding(xcb_im_client_t *client)
{
Clientenc *p, **link;
for(link = &clientenc; (p = *link) != nil; link = &p->next)
if(p->client == client){
*link = p->next;
free(p);
return;
}
}
static void
clearencodings(void)
{
Clientenc *next, *p;
for(p = clientenc; p != nil; p = next){
next = p->next;
free(p);
}
clientenc = nil;
}
static void
ximlog(char *msg)
@@ -219,7 +64,6 @@ kinit(void)
int32_t device;
int ok;
context = nil;
keymap = nil;
state = nil;
ok = -1;
@@ -253,45 +97,14 @@ static xcb_screen_t*
getscreen(int scr)
{
xcb_screen_iterator_t iter;
const xcb_setup_t *setup;
setup = xcb_get_setup(conn);
if(setup == nil)
return nil;
iter = xcb_setup_roots_iterator(setup);
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 void
readattrs(Ic *state)
{
const xcb_im_preedit_attr_t *attr;
xcb_window_t clientwin, focuswin;
xcb_point_t spot;
int hasspot;
clientwin = xcb_im_input_context_get_client_window(state->xic);
focuswin = xcb_im_input_context_get_focus_window(state->xic);
spot = state->spot;
hasspot = 0;
if(state->style == (XCB_IM_PreeditPosition | XCB_IM_StatusNothing) &&
(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(attr != nil){
spot = attr->spot_location;
hasspot = 1;
}
}
state->clientwin = clientwin;
state->focuswin = focuswin;
state->spot = spot;
state->hasspot = hasspot;
}
static int
translate(xcb_window_t win, int x, int y, Caret *caret)
{
@@ -335,32 +148,36 @@ placebottom(Ic *state, xcb_window_t win)
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)
{
xcb_window_t win[2];
int i, nwin;
const xcb_im_preedit_attr_t *attr;
xcb_window_t clientwin, focuswin;
memset(&state->caret, 0, sizeof state->caret);
win[0] = state->focuswin;
nwin = 1;
if(state->clientwin != state->focuswin)
win[nwin++] = state->clientwin;
if(state->hasspot && translate(state->focuswin,
state->spot.x, state->spot.y, &state->caret))
return;
for(i = 0; i < nwin; i++)
if(placebottom(state, win[i]))
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);
}
static void
refreshplace(Ic *state)
{
readattrs(state);
place(state);
}
/* XIM text goes out as COMPOUND_TEXT; imdkit wraps UTF-8 in ESC%G. */
static void
commit(Ic *state, char *s, int nbyte)
{
@@ -369,17 +186,12 @@ commit(Ic *state, char *s, int nbyte)
if(nbyte == 0)
return;
wire = s;
nwire = nbyte;
if(state->encoding == Ecompound){
wire = ximcompound(s, nbyte, &nwire);
if(wire == nil)
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);
if(wire != s)
free(wire);
free(wire);
}
static void
@@ -387,7 +199,7 @@ clearpreedit(Ic *state)
{
xcb_im_preedit_draw_fr_t frame;
if(state == nil || !state->prestarted)
if(!state->prestarted)
return;
memset(&frame, 0, sizeof frame);
frame.chg_length = state->nprerune;
@@ -410,20 +222,16 @@ updatepreedit(Ic *state, Str *pre)
size_t nwire;
int i, nbyte;
if(!(state->style & XCB_IM_PreeditCallbacks))
if(!(state->cap & Cclientpreedit))
return;
if(pre->n == 0){
clearpreedit(state);
return;
}
nbyte = stoutf(pre, utf, sizeof utf);
wire = utf;
nwire = nbyte;
if(state->encoding == Ecompound){
wire = ximcompound(utf, nbyte, &nwire);
if(wire == nil)
return;
}
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){
@@ -440,14 +248,7 @@ updatepreedit(Ic *state, Str *pre)
xcb_im_preedit_draw_callback(xim, state->xic, &frame);
state->nprerune = pre->n;
preowner = state;
if(wire != utf)
free(wire);
}
static int
meaningful(u32int key)
{
return key != 0 && (key < Kmodfirst || key > Kmodlast);
free(wire);
}
static void
@@ -467,6 +268,7 @@ sendrequest(Ic *state, int op, u32int key, u32int mod, Keyres *res)
chanrecv(replyc, res);
}
/* Another frontend may have taken the engine; then our preedit is stale. */
static void
checkpreowner(void)
{
@@ -479,17 +281,10 @@ checkpreowner(void)
clearpreedit(preowner);
}
static void
checkownerwake(void)
{
if(wakedrain() > 0)
checkpreowner();
}
static void
keypress(Ic *state, u32int key, u32int mod, Keyres *res)
{
if(meaningful(key)){
if(keymeaningful(key)){
if(preowner != nil && preowner != state)
clearpreedit(preowner);
state->engaged = 1;
@@ -502,8 +297,6 @@ release(Ic *state)
{
Keyres res;
if(state == nil)
return;
clearpreedit(state);
if(!state->engaged)
return;
@@ -511,6 +304,7 @@ release(Ic *state)
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)
{
@@ -519,9 +313,6 @@ resetic(Ic *state, xcb_im_reset_ic_reply_fr_t *reply)
size_t nwire;
int nbyte;
if(state == nil)
return;
sclear(&pending.preedit);
if(!state->engaged){
clearpreedit(state);
return;
@@ -532,14 +323,7 @@ resetic(Ic *state, xcb_im_reset_ic_reply_fr_t *reply)
if(reply == nil || !pending.eaten || pending.preedit.n == 0)
return;
nbyte = stoutf(&pending.preedit, utf, sizeof utf);
if(state->encoding == Ecompound)
wire = ximcompound(utf, nbyte, &nwire);
else{
nwire = nbyte;
wire = malloc(nwire);
if(wire != nil)
memmove(wire, utf, nwire);
}
wire = xcb_utf8_to_compound_text(utf, nbyte, &nwire);
if(wire == nil)
return;
reply->committed_string = (uchar*)wire;
@@ -556,9 +340,9 @@ kpress(Ic *state, xcb_key_press_event_t *ev)
key = keymaplookup(kstate, ev->detail, ev->state);
key = ipckeysym(key, xkb_keysym_to_utf32(key));
if(meaningful(key)){
if(keymeaningful(key)){
wasvalid = state->caret.valid;
refreshplace(state);
place(state);
if(state->engaged && wasvalid && !state->caret.valid)
sendrequest(state, Keycaret, 0, 0, &res);
}
@@ -596,67 +380,39 @@ icfree(void *p)
}
static void
setstyle(Ic *state, u32int style)
iccreate(xcb_im_client_t *client, xcb_im_input_context_t *ic)
{
int cap;
Ic *state;
if(!stylecap(style, &cap))
style = XCB_IM_PreeditNothing | XCB_IM_StatusNothing;
state->style = style;
state->cap = cap;
}
static void
icinstall(xcb_im_client_t *client, xcb_im_input_context_t *ic, Ic *state)
{
if(state == nil){
ximlog("out of memory; passing input context through");
xcb_im_input_context_set_data(ic, &passthrough, nil);
return;
}
state = emalloc(sizeof(Ic));
state->xic = ic;
state->client = client;
state->encoding = getencoding(client);
setstyle(state, xcb_im_input_context_get_input_style(ic));
refreshplace(state);
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
iccreate(xcb_im_client_t *client, xcb_im_input_context_t *ic)
{
if(ic == nil || xcb_im_input_context_get_data(ic) != nil)
return;
icinstall(client, ic, calloc(1, sizeof(Ic)));
}
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_ENCODING_NEGOTIATION){
if(arg != nil && setencoding(client,
*(u16int*)arg == 1 ? Eutf8 : Ecompound) < 0)
die("xim: cannot remember client encoding");
return;
}
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);
clearencoding(client);
return;
}
if(hdr->major_opcode == XCB_XIM_CREATE_IC){
iccreate(client, ic);
return;
@@ -664,16 +420,6 @@ callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic,
if(ic == nil)
return;
state = xcb_im_input_context_get_data(ic);
if(state == nil)
return;
if(state == &passthrough){
if(hdr->major_opcode == XCB_XIM_FORWARD_EVENT){
ev = arg;
if(ev != nil)
xcb_im_forward_event(xim, ic, ev);
}
return;
}
switch(hdr->major_opcode){
case XCB_XIM_FORWARD_EVENT:
ev = arg;
@@ -682,12 +428,9 @@ callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic,
break;
case XCB_XIM_SET_IC_VALUES:
case XCB_XIM_SET_IC_FOCUS:
refreshplace(state);
if(state->engaged){
Keyres res;
place(state);
if(state->engaged)
sendrequest(state, Keycaret, 0, 0, &res);
}
break;
case XCB_XIM_DESTROY_IC:
case XCB_XIM_UNSET_IC_FOCUS:
@@ -699,40 +442,6 @@ callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic,
}
}
static void
ximclose(void)
{
Ic *state, *next;
for(state = ics; state != nil; state = state->next)
release(state);
if(xim != nil){
if(opened)
xcb_im_close_im(xim);
xcb_im_destroy(xim);
xim = nil;
}
opened = 0;
preowner = nil;
clearencodings();
for(state = ics; state != nil; state = next){
next = state->next;
free(state);
}
ics = nil;
rootwin = XCB_NONE;
xkb_state_unref(kstate);
kstate = nil;
if(conn != nil){
xcb_disconnect(conn);
conn = nil;
}
if(replyc != nil){
chanfree(replyc);
replyc = nil;
}
}
static int
ximinit(void)
{
@@ -743,21 +452,13 @@ ximinit(void)
int scr;
replyc = chancreate(sizeof(Keyres), 0);
if(replyc == nil){
ximlog("cannot create reply channel");
return -1;
}
if(wakeinit() < 0){
ximlog("cannot create owner wake pipe");
return -1;
}
st.nStyles = nelem(styles);
st.styles = styles;
enc.nEncodings = nelem(encs);
enc.encodings = encs;
xcb_compound_text_init();
conn = xcb_connect(nil, &scr);
if(conn == nil || xcb_connection_has_error(conn)){
if(xcb_connection_has_error(conn)){
ximlog("cannot connect to X server");
return -1;
}
@@ -786,57 +487,42 @@ ximinit(void)
ximlog("cannot claim XIM selection");
return -1;
}
opened = 1;
return 0;
}
void
ximthread(void *arg)
{
struct pollfd pfd[2];
struct pollfd pfd;
xcb_generic_event_t *ev;
uint8_t type;
int fd, n;
int n;
USED(arg);
threadsetname("xim");
if(ximinit() < 0){
ximclose();
if(ximinit() < 0)
die("xim: initialization failed");
}
fd = xcb_get_file_descriptor(conn);
pfd[0].fd = fd;
pfd[0].events = POLLIN;
pfd[1].fd = wakefd[0];
pfd[1].events = POLLIN;
pfd.fd = xcb_get_file_descriptor(conn);
pfd.events = POLLIN;
for(;;){
pfd[0].revents = pfd[1].revents = 0;
n = poll(pfd, nelem(pfd), -1);
if(n < 0){
if(errno == EINTR)
continue;
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 == XCB_MAPPING_NOTIFY){
if(kinit() < 0)
ximlog("cannot refresh keyboard mapping");
}else
xcb_im_filter_event(xim, ev);
free(ev);
}
if(pfd[1].revents & POLLIN)
checkownerwake();
if(pfd[0].revents)
while((ev = xcb_poll_for_event(conn)) != nil){
type = ev->response_type & ~0x80;
if(type == XCB_MAPPING_NOTIFY){
if(kinit() < 0)
ximlog("cannot refresh keyboard mapping");
}else
xcb_im_filter_event(xim, ev);
free(ev);
/* Ownership changes outrank the next queued X event. */
checkownerwake();
}
if(pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL))
checkpreowner();
if(pfd.revents & (POLLERR|POLLHUP|POLLNVAL))
break;
}
n = xcb_connection_has_error(conn);
ximclose();
if(n)
if(xcb_connection_has_error(conn))
die("xim: X server disconnected");
die("xim: frontend stopped");
}

View File

@@ -1,45 +0,0 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <xcb-imdkit/encoding.h>
static char*
ximutf8compound(const char *s, size_t len, size_t *outlen)
{
static const char begin[] = "\033%G";
static const char end[] = "\033%@";
char *ct;
size_t n;
if(outlen != NULL)
*outlen = 0;
if(s == NULL || len > SIZE_MAX - (sizeof begin + sizeof end - 1))
return NULL;
n = len + sizeof begin + sizeof end - 2;
ct = malloc(n + 1);
if(ct == NULL)
return NULL;
memcpy(ct, begin, sizeof begin - 1);
memcpy(ct + sizeof begin - 1, s, len);
memcpy(ct + sizeof begin - 1 + len, end, sizeof end - 1);
ct[n] = '\0';
if(outlen != NULL)
*outlen = n;
return ct;
}
char*
ximcompound(const char *s, size_t len, size_t *outlen)
{
char *ct;
if(outlen != NULL)
*outlen = 0;
if(s == NULL)
return NULL;
ct = xcb_utf8_to_compound_text(s, len, outlen);
if(ct != NULL)
return ct;
/* X.Org Compound Text extension for characters in no legacy charset. */
return ximutf8compound(s, len, outlen);
}