Files
strans/xim.c
Hojun-Cho b979afc3f7 xim: a spot is a baseline; a popup flipped above it clears the line
An XIM spot has no height, so a popup that flips above it at the bottom
of the screen ended on the baseline and covered the line being typed.
The spot now stands for the row above it, one popup row tall, as GTK
and IBus carets carry their line height; below the spot nothing moves.
2026-08-17 01:59:44 +09:00

613 lines
14 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-compose.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 (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;
};
static xcb_connection_t *conn;
static xcb_im_t *xim;
static struct xkb_state *kstate;
static struct xkb_compose_state *compose;
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;
}
/* Dead keys and Compose: Xlib leaves them to the input method server. */
static void
cinit(void)
{
struct xkb_context *context;
struct xkb_compose_table *table;
char *locale;
locale = setlocale(LC_CTYPE, "");
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if(context == nil)
return;
table = xkb_compose_table_new_from_locale(context,
locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS);
if(table != nil)
compose = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS);
xkb_compose_table_unref(table);
xkb_context_unref(context);
}
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->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);
}
/* 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];
u32int key, sym;
int composed, n;
sym = keymaplookup(kstate, ev->detail, ev->state);
composed = 0;
if(compose != nil &&
xkb_compose_state_feed(compose, sym) == XKB_COMPOSE_FEED_ACCEPTED)
switch(xkb_compose_state_get_status(compose)){
case XKB_COMPOSE_COMPOSING:
case XKB_COMPOSE_CANCELLED:
return;
case XKB_COMPOSE_COMPOSED:
sym = xkb_compose_state_get_one_sym(compose);
composed = 1;
break;
default:
break;
}
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
if(keymeaningful(key))
place(state);
keypress(state, key, ev->state, &res);
n = stoutf(&res.commit, buf, sizeof buf);
commit(state, buf, n);
updatepreedit(state, &res.preedit);
if(res.eaten)
;
else if(composed){
/* The event holds the last key of the sequence, not its result. */
n = xkb_compose_state_get_utf8(compose, buf, sizeof buf);
commit(state, buf, min(n, (int)sizeof buf - 1));
}else
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)
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 = 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;
}
cinit();
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");
}