#include "dat.h" #include "fn.h" #include #include #include #include #include #include #include #include #include #include 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; }; 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, }; 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 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); } /* 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; } 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_keysym_to_utf8(sym, buf, sizeof buf); commit(state, buf, max(n - 1, 0)); }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 | 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; } 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"); }