Files
strans/xim/xim.c
Hojun-Cho a70cfc42e3 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.
2026-08-16 16:16:31 +09:00

529 lines
12 KiB
C

#include "dat.h"
#include "fn.h"
#include <errno.h>
#include <poll.h>
#include <xcb/xcb.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 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);
}
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, nil, 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 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;
}
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 == XCB_MAPPING_NOTIFY){
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");
}